blob: b49bc812df91ad9c9d4fc741b1da6744f8b89684 [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
Jack Rosenthale3a92672022-06-29 14:54:48 -060015import enum
Mike Frysingerc7796cf2020-02-06 23:55:15 -050016import functools
Mike Frysinger13f23a42013-05-13 17:32:01 -040017import inspect
Mike Frysinger87c74ce2017-04-04 16:12:31 -040018import json
Chris McDonald59650c32021-07-20 15:29:28 -060019import logging
Mike Frysinger2295d792021-03-08 15:55:23 -050020from pathlib import Path
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070021import re
Mike Frysinger2295d792021-03-08 15:55:23 -050022import shlex
Mike Frysinger87c74ce2017-04-04 16:12:31 -040023import sys
Mike Frysinger13f23a42013-05-13 17:32:01 -040024
Mike Frysinger2295d792021-03-08 15:55:23 -050025from chromite.lib import chromite_config
Chris McDonald59650c32021-07-20 15:29:28 -060026from chromite.lib import commandline
Aviv Keshetb7519e12016-10-04 00:50:00 -070027from chromite.lib import config_lib
28from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040029from chromite.lib import cros_build_lib
30from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050031from chromite.lib import gob_util
Mike Frysinger254f33f2019-12-11 13:54:29 -050032from chromite.lib import parallel
Mike Frysingera9751c92021-04-30 10:12:37 -040033from chromite.lib import retry_util
Mike Frysinger13f23a42013-05-13 17:32:01 -040034from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040035from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060036from chromite.utils import memoize
Alex Klein73eba212021-09-09 11:43:33 -060037from chromite.utils import pformat
Mike Frysinger13f23a42013-05-13 17:32:01 -040038
39
Mike Frysinger2295d792021-03-08 15:55:23 -050040class Config:
41 """Manage the user's gerrit config settings.
42
43 This is entirely unique to this gerrit command. Inspiration for naming and
44 layout is taken from ~/.gitconfig settings.
45 """
46
47 def __init__(self, path: Path = chromite_config.GERRIT_CONFIG):
48 self.cfg = configparser.ConfigParser(interpolation=None)
49 if path.exists():
50 self.cfg.read(chromite_config.GERRIT_CONFIG)
51
52 def expand_alias(self, action):
53 """Expand any aliases."""
54 alias = self.cfg.get('alias', action, fallback=None)
55 if alias is not None:
56 return shlex.split(alias)
57 return action
58
59
Mike Frysingerc7796cf2020-02-06 23:55:15 -050060class UserAction(object):
61 """Base class for all custom user actions."""
62
63 # The name of the command the user types in.
64 COMMAND = None
65
66 @staticmethod
67 def init_subparser(parser):
68 """Add arguments to this action's subparser."""
69
70 @staticmethod
71 def __call__(opts):
72 """Implement the action."""
Mike Frysinger62178ae2020-03-20 01:37:43 -040073 raise RuntimeError('Internal error: action missing __call__ implementation')
Mike Frysinger108eda22018-06-06 18:45:12 -040074
75
Mike Frysinger254f33f2019-12-11 13:54:29 -050076# How many connections we'll use in parallel. We don't want this to be too high
77# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
78# seems to be good enough for users.
79CONNECTION_LIMIT = 10
80
81
Mike Frysinger031ad0b2013-05-14 18:15:34 -040082COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040083
84# Map the internal names to the ones we normally show on the web ui.
85GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080086 'COMR': ['CQ', 'Commit Queue ',],
87 'CRVW': ['CR', 'Code Review ',],
88 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080089 'VRIF': ['V ', 'Verified ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060090 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040091}
92
93# Order is important -- matches the web ui. This also controls the short
94# entries that we summarize in non-verbose mode.
95GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
96
Mike Frysinger4aea5dc2019-07-17 13:39:56 -040097# Shorter strings for CL status messages.
98GERRIT_SUMMARY_MAP = {
99 'ABANDONED': 'ABD',
100 'MERGED': 'MRG',
101 'NEW': 'NEW',
102 'WIP': 'WIP',
103}
104
Mike Frysinger13f23a42013-05-13 17:32:01 -0400105
Jack Rosenthale3a92672022-06-29 14:54:48 -0600106class OutputFormat(enum.Enum):
107 """Type for the requested output format.
108
109 AUTO: Automatically determine the format based on what the user
110 might want. For now, this is just PRETTY, but this behavior is
111 subject to change.
112 RAW: Output CLs one per line, suitable for mild scripting.
113 JSON: JSON-encoded output, suitable for spicy scripting.
114 MARKDOWN: Suitable for posting in a bug or CL comment.
115 PRETTY: Suitable for viewing in a color terminal.
116 """
117 AUTO = 0
118 AUTOMATIC = AUTO
119 RAW = 1
120 JSON = 2
121 MARKDOWN = 3
122 PRETTY = 4
123
124
Mike Frysinger13f23a42013-05-13 17:32:01 -0400125def red(s):
126 return COLOR.Color(terminal.Color.RED, s)
127
128
129def green(s):
130 return COLOR.Color(terminal.Color.GREEN, s)
131
132
133def blue(s):
134 return COLOR.Color(terminal.Color.BLUE, s)
135
136
Mike Frysinger254f33f2019-12-11 13:54:29 -0500137def _run_parallel_tasks(task, *args):
138 """Small wrapper around BackgroundTaskRunner to enforce job count."""
Mike Frysingera9751c92021-04-30 10:12:37 -0400139 # When we run in parallel, we can hit the max requests limit.
140 def check_exc(e):
141 if not isinstance(e, gob_util.GOBError):
142 raise e
143 return e.http_status == 429
144
145 @retry_util.WithRetry(5, handler=check_exc, sleep=1, backoff_factor=2)
146 def retry(*args):
147 try:
148 task(*args)
149 except gob_util.GOBError as e:
150 if e.http_status != 429:
151 logging.warning('%s: skipping due: %s', args, e)
152 else:
153 raise
154
155 with parallel.BackgroundTaskRunner(retry, processes=CONNECTION_LIMIT) as q:
Mike Frysinger254f33f2019-12-11 13:54:29 -0500156 for arg in args:
157 q.put([arg])
158
159
Mike Frysinger13f23a42013-05-13 17:32:01 -0400160def limits(cls):
161 """Given a dict of fields, calculate the longest string lengths
162
163 This allows you to easily format the output of many results so that the
164 various cols all line up correctly.
165 """
166 lims = {}
167 for cl in cls:
168 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -0400169 # Use %s rather than str() to avoid codec issues.
170 # We also do this so we can format integers.
171 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400172 return lims
173
174
Mike Frysinger88f27292014-06-17 09:40:45 -0700175# TODO: This func really needs to be merged into the core gerrit logic.
176def GetGerrit(opts, cl=None):
177 """Auto pick the right gerrit instance based on the |cl|
178
179 Args:
180 opts: The general options object.
181 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
182
183 Returns:
184 A tuple of a gerrit object and a sanitized CL #.
185 """
186 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700187 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600188 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600189 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600190 if cl.startswith('*'):
191 cl = cl[1:]
192 else:
193 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700194 elif ':' in cl:
195 gob, cl = cl.split(':', 1)
196
197 if not gob in opts.gerrit:
198 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
199
200 return (opts.gerrit[gob], cl)
201
202
Mike Frysinger13f23a42013-05-13 17:32:01 -0400203def GetApprovalSummary(_opts, cls):
204 """Return a dict of the most important approvals"""
205 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700206 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
207 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
208 if not cats:
209 logging.warning('unknown gerrit approval type: %s', approver['type'])
210 continue
211 cat = cats[0].strip()
212 val = int(approver['value'])
213 if not cat in approvs:
214 # Ignore the extended categories in the summary view.
215 continue
216 elif approvs[cat] == '':
217 approvs[cat] = val
218 elif val < 0:
219 approvs[cat] = min(approvs[cat], val)
220 else:
221 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400222 return approvs
223
224
Mike Frysingera1b4b272017-04-05 16:11:00 -0400225def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400226 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400227 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400228 lims = {'url': 0, 'project': 0}
229
230 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400231
232 if opts.verbose:
233 status += '%s ' % (cl['status'],)
234 else:
235 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
236
Mike Frysinger13f23a42013-05-13 17:32:01 -0400237 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400238 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400239 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400240 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400241 functor = lambda x: x
242 elif approvs[cat] < 0:
243 functor = red
244 else:
245 functor = green
246 status += functor('%s:%2s ' % (cat, approvs[cat]))
247
Jack Rosenthale3a92672022-06-29 14:54:48 -0600248 if opts.format is OutputFormat.MARKDOWN:
Douglas Andersoncf9e9632022-05-24 14:55:16 -0700249 print('* %s - %s' % (uri_lib.ShortenUri(cl['url']), cl['subject']))
250 else:
251 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
252 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400253
254 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400255 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400256 functor = red if int(approver['value']) < 0 else green
257 n = functor('%2s' % approver['value'])
258 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
259 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500260 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400261
262
Mike Frysingera1b4b272017-04-05 16:11:00 -0400263def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400264 """Print all results based on the requested format."""
Jack Rosenthale3a92672022-06-29 14:54:48 -0600265 if opts.format is OutputFormat.RAW:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600266 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400267 pfx = ''
268 # Special case internal Chrome GoB as that is what most devs use.
269 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600270 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
271 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400272 for cl in cls:
273 print('%s%s' % (pfx, cl['number']))
274
Jack Rosenthale3a92672022-06-29 14:54:48 -0600275 elif opts.format is OutputFormat.JSON:
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400276 json.dump(cls, sys.stdout)
277
Mike Frysingera1b4b272017-04-05 16:11:00 -0400278 else:
279 if lims is None:
280 lims = limits(cls)
281
282 for cl in cls:
283 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
284
285
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400286def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700287 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800288 if opts.branch is not None:
289 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800290 if opts.project is not None:
291 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800292 if opts.topic is not None:
293 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800294
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400295 if helper is None:
296 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700297 return helper.Query(query, raw=raw, bypass_cache=False)
298
299
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400300def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700301 """Query gerrit and filter/clean up the results"""
302 ret = []
303
Mike Frysinger2cd56022017-01-12 20:56:27 -0500304 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400305 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400306 # Gerrit likes to return a stats record too.
307 if not 'project' in cl:
308 continue
309
310 # Strip off common leading names since the result is still
311 # unique over the whole tree.
312 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400313 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
314 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400315 if cl['project'].startswith('%s/' % pfx):
316 cl['project'] = cl['project'][len(pfx) + 1:]
317
Mike Frysinger479f1192017-09-14 22:36:30 -0400318 cl['url'] = uri_lib.ShortenUri(cl['url'])
319
Mike Frysinger13f23a42013-05-13 17:32:01 -0400320 ret.append(cl)
321
Mike Frysingerb62313a2017-06-30 16:38:58 -0400322 if opts.sort == 'unsorted':
323 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700324 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400325 key = lambda x: int(x[opts.sort])
326 else:
327 key = lambda x: x[opts.sort]
328 return sorted(ret, key=key)
329
330
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500331class _ActionSearchQuery(UserAction):
332 """Base class for actions that perform searches."""
333
334 @staticmethod
335 def init_subparser(parser):
336 """Add arguments to this action's subparser."""
337 parser.add_argument('--sort', default='number',
338 help='Key to sort on (number, project); use "unsorted" '
339 'to disable')
340 parser.add_argument('-b', '--branch',
341 help='Limit output to the specific branch')
342 parser.add_argument('-p', '--project',
343 help='Limit output to the specific project')
344 parser.add_argument('-t', '--topic',
345 help='Limit output to the specific topic')
346
347
348class ActionTodo(_ActionSearchQuery):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400349 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500350
351 COMMAND = 'todo'
352
353 @staticmethod
354 def __call__(opts):
355 """Implement the action."""
Mike Frysinger242d2922021-02-09 14:31:50 -0500356 cls = FilteredQuery(opts, 'attention:self')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500357 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400358
359
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500360class ActionSearch(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800361 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500362
363 COMMAND = 'search'
364
365 @staticmethod
366 def init_subparser(parser):
367 """Add arguments to this action's subparser."""
368 _ActionSearchQuery.init_subparser(parser)
369 parser.add_argument('query',
370 help='The search query')
371
372 @staticmethod
373 def __call__(opts):
374 """Implement the action."""
375 cls = FilteredQuery(opts, opts.query)
376 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400377
378
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500379class ActionMine(_ActionSearchQuery):
Mike Frysingera1db2c42014-06-15 00:42:48 -0700380 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500381
382 COMMAND = 'mine'
383
384 @staticmethod
385 def init_subparser(parser):
386 """Add arguments to this action's subparser."""
387 _ActionSearchQuery.init_subparser(parser)
388 parser.add_argument('--draft', default=False, action='store_true',
389 help='Show draft changes')
390
391 @staticmethod
392 def __call__(opts):
393 """Implement the action."""
394 if opts.draft:
395 rule = 'is:draft'
396 else:
397 rule = 'status:new'
398 cls = FilteredQuery(opts, 'owner:self %s' % (rule,))
399 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700400
401
Paul Hobbs89765232015-06-24 14:07:49 -0700402def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
403 """Runs breadth first search starting from the nodes in |to_visit|
404
405 Args:
406 to_visit: the starting nodes
407 children: a function which takes a node and returns the nodes adjacent to it
408 visited_key: a function for deduplicating node visits. Defaults to the
409 identity function (lambda x: x)
410
411 Returns:
412 A list of nodes which are reachable from any node in |to_visit| by calling
413 |children| any number of times.
414 """
415 to_visit = list(to_visit)
Mike Frysinger66ce4132019-07-17 22:52:52 -0400416 seen = set(visited_key(x) for x in to_visit)
Paul Hobbs89765232015-06-24 14:07:49 -0700417 for node in to_visit:
418 for child in children(node):
419 key = visited_key(child)
420 if key not in seen:
421 seen.add(key)
422 to_visit.append(child)
423 return to_visit
424
425
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500426class ActionDeps(_ActionSearchQuery):
Paul Hobbs89765232015-06-24 14:07:49 -0700427 """List CLs matching a query, and all transitive dependencies of those CLs"""
Paul Hobbs89765232015-06-24 14:07:49 -0700428
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500429 COMMAND = 'deps'
Paul Hobbs89765232015-06-24 14:07:49 -0700430
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500431 @staticmethod
432 def init_subparser(parser):
433 """Add arguments to this action's subparser."""
434 _ActionSearchQuery.init_subparser(parser)
435 parser.add_argument('query',
436 help='The search query')
437
438 def __call__(self, opts):
439 """Implement the action."""
440 cls = _Query(opts, opts.query, raw=False)
441
442 @memoize.Memoize
443 def _QueryChange(cl, helper=None):
444 return _Query(opts, cl, raw=False, helper=helper)
445
446 transitives = _BreadthFirstSearch(
Mike Nicholsa1414162021-04-22 20:07:22 +0000447 cls, functools.partial(self._Children, opts, _QueryChange),
Mike Frysingerdc407f52020-05-08 00:34:56 -0400448 visited_key=lambda cl: cl.PatchLink())
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500449
Mike Frysingerdc407f52020-05-08 00:34:56 -0400450 # This is a hack to avoid losing GoB host for each CL. The PrintCls
451 # function assumes the GoB host specified by the user is the only one
452 # that is ever used, but the deps command walks across hosts.
Jack Rosenthale3a92672022-06-29 14:54:48 -0600453 if opts.format is OutputFormat.RAW:
Mike Frysingerdc407f52020-05-08 00:34:56 -0400454 print('\n'.join(x.PatchLink() for x in transitives))
455 else:
456 transitives_raw = [cl.patch_dict for cl in transitives]
457 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500458
459 @staticmethod
460 def _ProcessDeps(opts, querier, cl, deps, required):
Mike Frysinger5726da92017-09-20 22:14:25 -0400461 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700462 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400463 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400464 if not dep.remote in opts.gerrit:
465 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
466 remote=dep.remote, print_cmd=opts.debug)
467 helper = opts.gerrit[dep.remote]
468
Paul Hobbs89765232015-06-24 14:07:49 -0700469 # TODO(phobbs) this should maybe catch network errors.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500470 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400471
472 # Handle empty results. If we found a commit that was pushed directly
473 # (e.g. a bot commit), then gerrit won't know about it.
474 if not changes:
475 if required:
476 logging.error('CL %s depends on %s which cannot be found',
477 cl, dep.ToGerritQueryText())
478 continue
479
480 # Our query might have matched more than one result. This can come up
481 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
482 # across multiple repos/branches. We blindly check all of them in the
483 # hopes that all open ones are what the user wants, but then again the
Alex Kleinea9cc822022-05-25 12:39:48 -0600484 # CQ-DEPEND syntax itself is unable to differentiate. *shrug*
Mike Frysinger5726da92017-09-20 22:14:25 -0400485 if len(changes) > 1:
486 logging.warning('CL %s has an ambiguous CQ dependency %s',
487 cl, dep.ToGerritQueryText())
488 for change in changes:
489 if change.status == 'NEW':
490 yield change
491
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500492 @classmethod
Mike Nicholsa1414162021-04-22 20:07:22 +0000493 def _Children(cls, opts, querier, cl):
Mike Frysinger7cbd88c2021-02-12 03:52:25 -0500494 """Yields the Gerrit dependencies of a patch"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500495 for change in cls._ProcessDeps(
496 opts, querier, cl, cl.GerritDependencies(), False):
Mike Frysinger5726da92017-09-20 22:14:25 -0400497 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700498
Paul Hobbs89765232015-06-24 14:07:49 -0700499
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500500class ActionInspect(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800501 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500502
503 COMMAND = 'inspect'
504
505 @staticmethod
506 def init_subparser(parser):
507 """Add arguments to this action's subparser."""
508 _ActionSearchQuery.init_subparser(parser)
509 parser.add_argument('cls', nargs='+', metavar='CL',
510 help='The CL(s) to update')
511
512 @staticmethod
513 def __call__(opts):
514 """Implement the action."""
515 cls = []
516 for arg in opts.cls:
517 helper, cl = GetGerrit(opts, arg)
518 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
519 if change:
520 cls.extend(change)
521 else:
522 logging.warning('no results found for CL %s', arg)
523 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400524
525
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500526class _ActionLabeler(UserAction):
527 """Base helper for setting labels."""
528
529 LABEL = None
530 VALUES = None
531
532 @classmethod
533 def init_subparser(cls, parser):
534 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500535 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
536 help='Optional message to include')
537 parser.add_argument('cls', nargs='+', metavar='CL',
538 help='The CL(s) to update')
539 parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
540 help='The label value; one of [%(choices)s]')
541
542 @classmethod
543 def __call__(cls, opts):
544 """Implement the action."""
Alex Kleinea9cc822022-05-25 12:39:48 -0600545 # Convert user-friendly command line option into a gerrit parameter.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500546 def task(arg):
547 helper, cl = GetGerrit(opts, arg)
548 helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
549 dryrun=opts.dryrun, notify=opts.notify)
550 _run_parallel_tasks(task, *opts.cls)
551
552
553class ActionLabelAutoSubmit(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500554 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500555
556 COMMAND = 'label-as'
557 LABEL = 'Auto-Submit'
558 VALUES = ('0', '1')
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600559
560
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500561class ActionLabelCodeReview(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500562 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500563
564 COMMAND = 'label-cr'
565 LABEL = 'Code-Review'
566 VALUES = ('-2', '-1', '0', '1', '2')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400567
568
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500569class ActionLabelVerified(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500570 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500571
572 COMMAND = 'label-v'
573 LABEL = 'Verified'
574 VALUES = ('-1', '0', '1')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400575
576
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500577class ActionLabelCommitQueue(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500578 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500579
580 COMMAND = 'label-cq'
581 LABEL = 'Commit-Queue'
582 VALUES = ('0', '1', '2')
Mike Frysinger15b23e42014-12-05 17:00:05 -0500583
C Shapiro3f1f8242021-08-02 15:28:29 -0500584class ActionLabelOwnersOverride(_ActionLabeler):
585 """Change the Owners-Override label (1=Override)"""
586
587 COMMAND = 'label-oo'
588 LABEL = 'Owners-Override'
589 VALUES = ('0', '1')
590
Mike Frysinger15b23e42014-12-05 17:00:05 -0500591
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500592class _ActionSimpleParallelCLs(UserAction):
593 """Base helper for actions that only accept CLs."""
594
595 @staticmethod
596 def init_subparser(parser):
597 """Add arguments to this action's subparser."""
598 parser.add_argument('cls', nargs='+', metavar='CL',
599 help='The CL(s) to update')
600
601 def __call__(self, opts):
602 """Implement the action."""
603 def task(arg):
604 helper, cl = GetGerrit(opts, arg)
605 self._process_one(helper, cl, opts)
606 _run_parallel_tasks(task, *opts.cls)
607
608
609class ActionSubmit(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800610 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500611
612 COMMAND = 'submit'
613
614 @staticmethod
615 def _process_one(helper, cl, opts):
616 """Use |helper| to process the single |cl|."""
Mike Frysinger8674a112021-02-09 14:44:17 -0500617 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400618
619
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500620class ActionAbandon(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800621 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500622
623 COMMAND = 'abandon'
624
625 @staticmethod
Mike Frysinger3af378b2021-03-12 01:34:04 -0500626 def init_subparser(parser):
627 """Add arguments to this action's subparser."""
628 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
629 help='Include a message')
630 _ActionSimpleParallelCLs.init_subparser(parser)
631
632 @staticmethod
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500633 def _process_one(helper, cl, opts):
634 """Use |helper| to process the single |cl|."""
Mike Frysinger3af378b2021-03-12 01:34:04 -0500635 helper.AbandonChange(cl, msg=opts.msg, dryrun=opts.dryrun,
636 notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400637
638
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500639class ActionRestore(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800640 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500641
642 COMMAND = 'restore'
643
644 @staticmethod
645 def _process_one(helper, cl, opts):
646 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700647 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400648
649
Tomasz Figa54d70992021-01-20 13:48:59 +0900650class ActionWorkInProgress(_ActionSimpleParallelCLs):
651 """Mark CLs as work in progress"""
652
653 COMMAND = 'wip'
654
655 @staticmethod
656 def _process_one(helper, cl, opts):
657 """Use |helper| to process the single |cl|."""
658 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
659
660
661class ActionReadyForReview(_ActionSimpleParallelCLs):
662 """Mark CLs as ready for review"""
663
664 COMMAND = 'ready'
665
666 @staticmethod
667 def _process_one(helper, cl, opts):
668 """Use |helper| to process the single |cl|."""
669 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
670
671
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500672class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800673 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700674
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500675 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700676
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500677 @staticmethod
678 def init_subparser(parser):
679 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500680 parser.add_argument('cl', metavar='CL',
681 help='The CL to update')
682 parser.add_argument('reviewers', nargs='+',
683 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700684
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500685 @staticmethod
686 def __call__(opts):
687 """Implement the action."""
688 # Allow for optional leading '~'.
689 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
690 add_list, remove_list, invalid_list = [], [], []
691
692 for email in opts.reviewers:
693 if not email_validator.match(email):
694 invalid_list.append(email)
695 elif email[0] == '~':
696 remove_list.append(email[1:])
697 else:
698 add_list.append(email)
699
700 if invalid_list:
701 cros_build_lib.Die(
702 'Invalid email address(es): %s' % ', '.join(invalid_list))
703
704 if add_list or remove_list:
705 helper, cl = GetGerrit(opts, opts.cl)
706 helper.SetReviewers(cl, add=add_list, remove=remove_list,
707 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700708
709
Brian Norrisd25af082021-10-29 11:25:31 -0700710class ActionAttentionSet(UserAction):
711 """Add/remove emails from the attention set (prepend with '~' to remove)"""
712
713 COMMAND = 'attention'
714
715 @staticmethod
716 def init_subparser(parser):
717 """Add arguments to this action's subparser."""
718 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
719 help='Optional message to include',
720 default='gerrit CLI')
721 parser.add_argument('cl', metavar='CL',
722 help='The CL to update')
723 parser.add_argument('users', nargs='+',
724 help='The users to add/remove from attention set')
725
726 @staticmethod
727 def __call__(opts):
728 """Implement the action."""
729 # Allow for optional leading '~'.
730 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
731 add_list, remove_list, invalid_list = [], [], []
732
733 for email in opts.users:
734 if not email_validator.match(email):
735 invalid_list.append(email)
736 elif email[0] == '~':
737 remove_list.append(email[1:])
738 else:
739 add_list.append(email)
740
741 if invalid_list:
742 cros_build_lib.Die(
743 'Invalid email address(es): %s' % ', '.join(invalid_list))
744
745 if add_list or remove_list:
746 helper, cl = GetGerrit(opts, opts.cl)
747 helper.SetAttentionSet(cl, add=add_list, remove=remove_list,
748 dryrun=opts.dryrun, notify=opts.notify,
749 message=opts.msg)
750
751
Mike Frysinger62178ae2020-03-20 01:37:43 -0400752class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800753 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500754
755 COMMAND = 'message'
756
757 @staticmethod
758 def init_subparser(parser):
759 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400760 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500761 parser.add_argument('message',
762 help='The message to post')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500763
764 @staticmethod
765 def _process_one(helper, cl, opts):
766 """Use |helper| to process the single |cl|."""
767 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530768
769
Mike Frysinger62178ae2020-03-20 01:37:43 -0400770class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800771 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500772
773 COMMAND = 'topic'
774
775 @staticmethod
776 def init_subparser(parser):
777 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400778 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500779 parser.add_argument('topic',
780 help='The topic to set')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500781
782 @staticmethod
783 def _process_one(helper, cl, opts):
784 """Use |helper| to process the single |cl|."""
785 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800786
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800787
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500788class ActionPrivate(_ActionSimpleParallelCLs):
789 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700790
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500791 COMMAND = 'private'
792
793 @staticmethod
794 def _process_one(helper, cl, opts):
795 """Use |helper| to process the single |cl|."""
796 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700797
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800798
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500799class ActionPublic(_ActionSimpleParallelCLs):
800 """Mark CLs public"""
801
802 COMMAND = 'public'
803
804 @staticmethod
805 def _process_one(helper, cl, opts):
806 """Use |helper| to process the single |cl|."""
807 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
808
809
810class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800811 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500812
813 COMMAND = 'hashtags'
814
815 @staticmethod
816 def init_subparser(parser):
817 """Add arguments to this action's subparser."""
818 parser.add_argument('cl', metavar='CL',
819 help='The CL to update')
820 parser.add_argument('hashtags', nargs='+',
821 help='The hashtags to add/remove')
822
823 @staticmethod
824 def __call__(opts):
825 """Implement the action."""
826 add = []
827 remove = []
828 for hashtag in opts.hashtags:
829 if hashtag.startswith('~'):
830 remove.append(hashtag[1:])
831 else:
832 add.append(hashtag)
833 helper, cl = GetGerrit(opts, opts.cl)
834 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800835
836
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500837class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800838 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500839
840 COMMAND = 'deletedraft'
841
842 @staticmethod
843 def _process_one(helper, cl, opts):
844 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700845 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800846
847
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500848class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500849 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500850
851 COMMAND = 'reviewed'
852
853 @staticmethod
854 def _process_one(helper, cl, opts):
855 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500856 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500857
858
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500859class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500860 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500861
862 COMMAND = 'unreviewed'
863
864 @staticmethod
865 def _process_one(helper, cl, opts):
866 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500867 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500868
869
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500870class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500871 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500872
873 COMMAND = 'ignore'
874
875 @staticmethod
876 def _process_one(helper, cl, opts):
877 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500878 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500879
880
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500881class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500882 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500883
884 COMMAND = 'unignore'
885
886 @staticmethod
887 def _process_one(helper, cl, opts):
888 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500889 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500890
891
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400892class ActionCherryPick(UserAction):
Alex Kleinea9cc822022-05-25 12:39:48 -0600893 """Cherry-pick CLs to branches."""
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400894
895 COMMAND = 'cherry-pick'
896
897 @staticmethod
898 def init_subparser(parser):
899 """Add arguments to this action's subparser."""
900 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
901 parser.add_argument('--rev', '--revision', default='current',
902 help='A specific revision or patchset')
903 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
904 help='Include a message')
905 parser.add_argument('--branches', '--branch', '--br', action='split_extend',
906 default=[], required=True,
907 help='The destination branches')
908 parser.add_argument('cls', nargs='+', metavar='CL',
909 help='The CLs to cherry-pick')
910
911 @staticmethod
912 def __call__(opts):
913 """Implement the action."""
914 # Process branches in parallel, but CLs in serial in case of CL stacks.
915 def task(branch):
916 for arg in opts.cls:
917 helper, cl = GetGerrit(opts, arg)
918 ret = helper.CherryPick(cl, branch, rev=opts.rev, msg=opts.msg,
Mike Frysinger8674a112021-02-09 14:44:17 -0500919 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400920 logging.debug('Response: %s', ret)
Jack Rosenthale3a92672022-06-29 14:54:48 -0600921 if opts.format is OutputFormat.RAW:
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400922 print(ret['_number'])
923 else:
924 uri = f'https://{helper.host}/c/{ret["_number"]}'
925 print(uri_lib.ShortenUri(uri))
926
927 _run_parallel_tasks(task, *opts.branches)
928
929
Mike Frysinger8037f752020-02-29 20:47:09 -0500930class ActionReview(_ActionSimpleParallelCLs):
931 """Review CLs with multiple settings
932
933 The label option supports extended/multiple syntax for easy use. The --label
934 option may be specified multiple times (as settings are merges), and multiple
935 labels are allowed in a single argument. Each label has the form:
936 <long or short name><=+-><value>
937
938 Common arguments:
939 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
940 'V+1 CQ+2'
941 'AS=1 V=1'
942 """
943
944 COMMAND = 'review'
945
946 class _SetLabel(argparse.Action):
947 """Argparse action for setting labels."""
948
949 LABEL_MAP = {
950 'AS': 'Auto-Submit',
951 'CQ': 'Commit-Queue',
952 'CR': 'Code-Review',
953 'V': 'Verified',
954 }
955
956 def __call__(self, parser, namespace, values, option_string=None):
957 labels = getattr(namespace, self.dest)
958 for request in values.split():
959 if '=' in request:
960 # Handle Verified=1 form.
961 short, value = request.split('=', 1)
962 elif '+' in request:
963 # Handle Verified+1 form.
964 short, value = request.split('+', 1)
965 elif '-' in request:
966 # Handle Verified-1 form.
967 short, value = request.split('-', 1)
968 value = '-%s' % (value,)
969 else:
970 parser.error('Invalid label setting "%s". Must be Commit-Queue=1 or '
971 'CQ+1 or CR-1.' % (request,))
972
973 # Convert possible short label names like "V" to "Verified".
974 label = self.LABEL_MAP.get(short)
975 if not label:
976 label = short
977
978 # We allow existing label requests to be overridden.
979 labels[label] = value
980
981 @classmethod
982 def init_subparser(cls, parser):
983 """Add arguments to this action's subparser."""
984 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
985 help='Include a message')
986 parser.add_argument('-l', '--label', dest='labels',
987 action=cls._SetLabel, default={},
988 help='Set a label with a value')
989 parser.add_argument('--ready', default=None, action='store_true',
990 help='Set CL status to ready-for-review')
991 parser.add_argument('--wip', default=None, action='store_true',
992 help='Set CL status to WIP')
993 parser.add_argument('--reviewers', '--re', action='append', default=[],
994 help='Add reviewers')
995 parser.add_argument('--cc', action='append', default=[],
996 help='Add people to CC')
997 _ActionSimpleParallelCLs.init_subparser(parser)
998
999 @staticmethod
1000 def _process_one(helper, cl, opts):
1001 """Use |helper| to process the single |cl|."""
1002 helper.SetReview(cl, msg=opts.msg, labels=opts.labels, dryrun=opts.dryrun,
1003 notify=opts.notify, reviewers=opts.reviewers, cc=opts.cc,
1004 ready=opts.ready, wip=opts.wip)
1005
1006
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001007class ActionAccount(_ActionSimpleParallelCLs):
1008 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001009
1010 COMMAND = 'account'
1011
1012 @staticmethod
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001013 def init_subparser(parser):
1014 """Add arguments to this action's subparser."""
1015 parser.add_argument('accounts', nargs='*', default=['self'],
1016 help='The accounts to query')
1017
1018 @classmethod
1019 def __call__(cls, opts):
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001020 """Implement the action."""
1021 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001022
1023 def print_one(header, data):
1024 print(f'### {header}')
Jack Rosenthale3a92672022-06-29 14:54:48 -06001025 compact = opts.format is OutputFormat.JSON
1026 print(pformat.json(data, compact=compact).rstrip())
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001027
1028 def task(arg):
1029 detail = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/detail')
1030 if not detail:
1031 print(f'{arg}: account not found')
1032 else:
1033 print_one('detail', detail)
1034 for field in ('groups', 'capabilities', 'preferences', 'sshkeys',
1035 'gpgkeys'):
1036 data = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/{field}')
1037 print_one(field, data)
1038
1039 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -08001040
1041
Mike Frysinger2295d792021-03-08 15:55:23 -05001042class ActionConfig(UserAction):
1043 """Manage the gerrit tool's own config file
1044
1045 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
1046 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
1047
1048 # Set up subcommand aliases.
1049 [alias]
1050 common-search = search 'is:open project:something/i/care/about'
1051 """
1052
1053 COMMAND = 'config'
1054
1055 @staticmethod
1056 def __call__(opts):
1057 """Implement the action."""
1058 # For now, this is a place holder for raising visibility for the config file
1059 # and its associated help text documentation.
1060 opts.parser.parse_args(['config', '--help'])
1061
1062
Mike Frysingere5450602021-03-08 15:34:17 -05001063class ActionHelp(UserAction):
1064 """An alias to --help for CLI symmetry"""
1065
1066 COMMAND = 'help'
1067
1068 @staticmethod
1069 def init_subparser(parser):
1070 """Add arguments to this action's subparser."""
1071 parser.add_argument('command', nargs='?',
1072 help='The command to display.')
1073
1074 @staticmethod
1075 def __call__(opts):
1076 """Implement the action."""
1077 # Show global help.
1078 if not opts.command:
1079 opts.parser.print_help()
1080 return
1081
1082 opts.parser.parse_args([opts.command, '--help'])
1083
1084
Mike Frysinger484e2f82020-03-20 01:41:10 -04001085class ActionHelpAll(UserAction):
1086 """Show all actions help output at once."""
1087
1088 COMMAND = 'help-all'
1089
1090 @staticmethod
1091 def __call__(opts):
1092 """Implement the action."""
1093 first = True
1094 for action in _GetActions():
1095 if first:
1096 first = False
1097 else:
1098 print('\n\n')
1099
1100 try:
1101 opts.parser.parse_args([action, '--help'])
1102 except SystemExit:
1103 pass
1104
1105
Mike Frysinger65fc8632020-02-06 18:11:12 -05001106@memoize.Memoize
1107def _GetActions():
1108 """Get all the possible actions we support.
1109
1110 Returns:
1111 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1112 function that implements that command (e.g. UserActFoo).
1113 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001114 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
1115
1116 actions = {}
1117 for cls in globals().values():
1118 if (not inspect.isclass(cls) or
1119 not issubclass(cls, UserAction) or
1120 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -05001121 continue
1122
Mike Frysinger65fc8632020-02-06 18:11:12 -05001123 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001124 cmd = cls.COMMAND
1125 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1126 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001127
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001128 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001129
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001130 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001131
1132
Harry Cutts26076b32019-02-26 15:01:29 -08001133def _GetActionUsages():
1134 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -05001135 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001136
Mike Frysinger65fc8632020-02-06 18:11:12 -05001137 cmds = list(actions.keys())
1138 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -08001139 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001140 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001141
Harry Cutts26076b32019-02-26 15:01:29 -08001142 cmd_indent = len(max(cmds, key=len))
1143 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001144 return '\n'.join(
1145 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
1146 for cmd, usage, doc in zip(cmds, usages, docs)
1147 )
Harry Cutts26076b32019-02-26 15:01:29 -08001148
1149
Mike Frysinger2295d792021-03-08 15:55:23 -05001150def _AddCommonOptions(parser, subparser):
1151 """Add options that should work before & after the subcommand.
1152
1153 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1154 """
1155 parser.add_common_argument_to_group(
1156 subparser, '--ne', '--no-emails', dest='notify',
1157 default='ALL', action='store_const', const='NONE',
1158 help='Do not send e-mail notifications')
1159 parser.add_common_argument_to_group(
1160 subparser, '-n', '--dry-run', dest='dryrun',
1161 default=False, action='store_true',
1162 help='Show what would be done, but do not make changes')
1163
1164
1165def GetBaseParser() -> commandline.ArgumentParser:
1166 """Returns the common parser (i.e. no subparsers added)."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001167 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001168There is no support for doing line-by-line code review via the command line.
1169This helps you manage various bits and CL status.
1170
Mike Frysingera1db2c42014-06-15 00:42:48 -07001171For general Gerrit documentation, see:
1172 https://gerrit-review.googlesource.com/Documentation/
1173The Searching Changes page covers the search query syntax:
1174 https://gerrit-review.googlesource.com/Documentation/user-search.html
1175
Mike Frysinger13f23a42013-05-13 17:32:01 -04001176Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001177 $ gerrit todo # List all the CLs that await your review.
1178 $ gerrit mine # List all of your open CLs.
1179 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1180 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1181 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001182 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
118328123.
1184 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1185CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001186Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001187 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1188with Commit-Queue=1.
1189 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1190CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001191 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001192
Harry Cutts26076b32019-02-26 15:01:29 -08001193Actions:
1194"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001195 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001196
Alex Klein2ab29cc2018-07-19 12:01:00 -06001197 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -04001198 parser = commandline.ArgumentParser(
Mike Frysinger50917ad2022-04-12 20:37:14 -04001199 description=description, default_log_level='notice',
1200 epilog='For subcommand help, use `gerrit help <command>`.')
Mike Frysinger8674a112021-02-09 14:44:17 -05001201
1202 group = parser.add_argument_group('Server options')
1203 group.add_argument('-i', '--internal', dest='gob', action='store_const',
1204 default=site_params.EXTERNAL_GOB_INSTANCE,
1205 const=site_params.INTERNAL_GOB_INSTANCE,
1206 help='Query internal Chrome Gerrit instance')
1207 group.add_argument('-g', '--gob',
1208 default=site_params.EXTERNAL_GOB_INSTANCE,
Brian Norrisd25af082021-10-29 11:25:31 -07001209 help=('Gerrit (on borg) instance to query '
1210 '(default: %(default)s)'))
Mike Frysinger8674a112021-02-09 14:44:17 -05001211
Mike Frysinger8674a112021-02-09 14:44:17 -05001212 group = parser.add_argument_group('CL options')
Mike Frysinger2295d792021-03-08 15:55:23 -05001213 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001214
Jack Rosenthale3a92672022-06-29 14:54:48 -06001215 group = parser.add_mutually_exclusive_group()
1216 parser.set_defaults(format=OutputFormat.AUTO)
1217 group.add_argument(
1218 '--format',
1219 action='enum',
1220 enum=OutputFormat,
1221 help='Output format to use.',
1222 )
1223 group.add_argument(
1224 '--raw',
1225 action='store_const',
1226 dest='format',
1227 const=OutputFormat.RAW,
1228 help='Alias for --format=raw.',
1229 )
1230 group.add_argument(
1231 '--json',
1232 action='store_const',
1233 dest='format',
1234 const=OutputFormat.JSON,
1235 help='Alias for --format=json.',
1236 )
Mike Frysinger2295d792021-03-08 15:55:23 -05001237 return parser
1238
1239
1240def GetParser(parser: commandline.ArgumentParser = None) -> (
1241 commandline.ArgumentParser):
1242 """Returns the full parser to use for this module."""
1243 if parser is None:
1244 parser = GetBaseParser()
1245
1246 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001247
1248 # Subparsers are required by default under Python 2. Python 3 changed to
1249 # not required, but didn't include a required option until 3.7. Setting
1250 # the required member works in all versions (and setting dest name).
1251 subparsers = parser.add_subparsers(dest='action')
1252 subparsers.required = True
1253 for cmd, cls in actions.items():
1254 # Format the full docstring by removing the file level indentation.
1255 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
1256 subparser = subparsers.add_parser(cmd, description=description)
Mike Frysinger2295d792021-03-08 15:55:23 -05001257 _AddCommonOptions(parser, subparser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001258 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001259
1260 return parser
1261
1262
1263def main(argv):
Mike Frysinger2295d792021-03-08 15:55:23 -05001264 base_parser = GetBaseParser()
1265 opts, subargs = base_parser.parse_known_args(argv)
1266
1267 config = Config()
1268 if subargs:
1269 # If the action is an alias to an expanded value, we need to mutate the argv
1270 # and reparse things.
1271 action = config.expand_alias(subargs[0])
1272 if action != subargs[0]:
1273 pos = argv.index(subargs[0])
1274 argv = argv[:pos] + action + argv[pos + 1:]
1275
1276 parser = GetParser(parser=base_parser)
Mike Frysingerddf86eb2014-02-07 22:51:41 -05001277 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001278
Mike Frysinger484e2f82020-03-20 01:41:10 -04001279 # In case the action wants to throw a parser error.
1280 opts.parser = parser
1281
Mike Frysinger88f27292014-06-17 09:40:45 -07001282 # A cache of gerrit helpers we'll load on demand.
1283 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001284
Jack Rosenthale3a92672022-06-29 14:54:48 -06001285 if opts.format is OutputFormat.AUTO:
1286 opts.format = OutputFormat.PRETTY
1287
Mike Frysinger88f27292014-06-17 09:40:45 -07001288 opts.Freeze()
1289
Mike Frysinger27e21b72018-07-12 14:20:21 -04001290 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001291 global COLOR
1292 COLOR = terminal.Color(enabled=opts.color)
1293
Mike Frysinger13f23a42013-05-13 17:32:01 -04001294 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001295 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001296 obj = actions[opts.action]()
Mike Frysinger65fc8632020-02-06 18:11:12 -05001297 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001298 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001299 except (cros_build_lib.RunCommandError, gerrit.GerritException,
1300 gob_util.GOBError) as e:
1301 cros_build_lib.Die(e)