blob: 5081b303921394d6a8a64c0a00df03da3c2af18f [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Mike Frysinger13f23a42013-05-13 17:32:01 -04002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysinger08737512014-02-07 22:58:26 -05006"""A command line interface to Gerrit-on-borg instances.
Mike Frysinger13f23a42013-05-13 17:32:01 -04007
8Internal Note:
9To expose a function directly to the command line interface, name your function
10with the prefix "UserAct".
11"""
12
Mike Frysinger31ff6f92014-02-08 04:33:03 -050013from __future__ import print_function
14
Mike Frysinger8037f752020-02-29 20:47:09 -050015import argparse
Mike Frysinger65fc8632020-02-06 18:11:12 -050016import collections
Mike Frysinger2295d792021-03-08 15:55:23 -050017import configparser
Mike Frysingerc7796cf2020-02-06 23:55:15 -050018import functools
Mike Frysinger13f23a42013-05-13 17:32:01 -040019import inspect
Mike Frysinger87c74ce2017-04-04 16:12:31 -040020import json
Mike Frysinger2295d792021-03-08 15:55:23 -050021from pathlib import Path
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070022import re
Mike Frysinger2295d792021-03-08 15:55:23 -050023import shlex
Mike Frysinger87c74ce2017-04-04 16:12:31 -040024import sys
Mike Frysinger13f23a42013-05-13 17:32:01 -040025
Mike Frysinger2295d792021-03-08 15:55:23 -050026from chromite.lib import chromite_config
Aviv Keshetb7519e12016-10-04 00:50:00 -070027from chromite.lib import config_lib
28from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040029from chromite.lib import commandline
30from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070031from chromite.lib import cros_logging as logging
Mike Frysinger13f23a42013-05-13 17:32:01 -040032from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050033from chromite.lib import gob_util
Mike Frysinger254f33f2019-12-11 13:54:29 -050034from chromite.lib import parallel
Mike Frysinger7f2018d2021-02-04 00:10:58 -050035from chromite.lib import pformat
Mike Frysingera9751c92021-04-30 10:12:37 -040036from chromite.lib import retry_util
Mike Frysinger13f23a42013-05-13 17:32:01 -040037from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040038from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060039from chromite.utils import memoize
Mike Frysinger13f23a42013-05-13 17:32:01 -040040
41
Mike Frysinger1c76d4c2020-02-08 23:35:29 -050042assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
43
44
Mike Frysinger2295d792021-03-08 15:55:23 -050045class Config:
46 """Manage the user's gerrit config settings.
47
48 This is entirely unique to this gerrit command. Inspiration for naming and
49 layout is taken from ~/.gitconfig settings.
50 """
51
52 def __init__(self, path: Path = chromite_config.GERRIT_CONFIG):
53 self.cfg = configparser.ConfigParser(interpolation=None)
54 if path.exists():
55 self.cfg.read(chromite_config.GERRIT_CONFIG)
56
57 def expand_alias(self, action):
58 """Expand any aliases."""
59 alias = self.cfg.get('alias', action, fallback=None)
60 if alias is not None:
61 return shlex.split(alias)
62 return action
63
64
Mike Frysingerc7796cf2020-02-06 23:55:15 -050065class UserAction(object):
66 """Base class for all custom user actions."""
67
68 # The name of the command the user types in.
69 COMMAND = None
70
71 @staticmethod
72 def init_subparser(parser):
73 """Add arguments to this action's subparser."""
74
75 @staticmethod
76 def __call__(opts):
77 """Implement the action."""
Mike Frysinger62178ae2020-03-20 01:37:43 -040078 raise RuntimeError('Internal error: action missing __call__ implementation')
Mike Frysinger108eda22018-06-06 18:45:12 -040079
80
Mike Frysinger254f33f2019-12-11 13:54:29 -050081# How many connections we'll use in parallel. We don't want this to be too high
82# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
83# seems to be good enough for users.
84CONNECTION_LIMIT = 10
85
86
Mike Frysinger031ad0b2013-05-14 18:15:34 -040087COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040088
89# Map the internal names to the ones we normally show on the web ui.
90GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080091 'COMR': ['CQ', 'Commit Queue ',],
92 'CRVW': ['CR', 'Code Review ',],
93 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080094 'VRIF': ['V ', 'Verified ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060095 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040096}
97
98# Order is important -- matches the web ui. This also controls the short
99# entries that we summarize in non-verbose mode.
100GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
101
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400102# Shorter strings for CL status messages.
103GERRIT_SUMMARY_MAP = {
104 'ABANDONED': 'ABD',
105 'MERGED': 'MRG',
106 'NEW': 'NEW',
107 'WIP': 'WIP',
108}
109
Mike Frysinger13f23a42013-05-13 17:32:01 -0400110
111def red(s):
112 return COLOR.Color(terminal.Color.RED, s)
113
114
115def green(s):
116 return COLOR.Color(terminal.Color.GREEN, s)
117
118
119def blue(s):
120 return COLOR.Color(terminal.Color.BLUE, s)
121
122
Mike Frysinger254f33f2019-12-11 13:54:29 -0500123def _run_parallel_tasks(task, *args):
124 """Small wrapper around BackgroundTaskRunner to enforce job count."""
Mike Frysingera9751c92021-04-30 10:12:37 -0400125 # When we run in parallel, we can hit the max requests limit.
126 def check_exc(e):
127 if not isinstance(e, gob_util.GOBError):
128 raise e
129 return e.http_status == 429
130
131 @retry_util.WithRetry(5, handler=check_exc, sleep=1, backoff_factor=2)
132 def retry(*args):
133 try:
134 task(*args)
135 except gob_util.GOBError as e:
136 if e.http_status != 429:
137 logging.warning('%s: skipping due: %s', args, e)
138 else:
139 raise
140
141 with parallel.BackgroundTaskRunner(retry, processes=CONNECTION_LIMIT) as q:
Mike Frysinger254f33f2019-12-11 13:54:29 -0500142 for arg in args:
143 q.put([arg])
144
145
Mike Frysinger13f23a42013-05-13 17:32:01 -0400146def limits(cls):
147 """Given a dict of fields, calculate the longest string lengths
148
149 This allows you to easily format the output of many results so that the
150 various cols all line up correctly.
151 """
152 lims = {}
153 for cl in cls:
154 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -0400155 # Use %s rather than str() to avoid codec issues.
156 # We also do this so we can format integers.
157 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400158 return lims
159
160
Mike Frysinger88f27292014-06-17 09:40:45 -0700161# TODO: This func really needs to be merged into the core gerrit logic.
162def GetGerrit(opts, cl=None):
163 """Auto pick the right gerrit instance based on the |cl|
164
165 Args:
166 opts: The general options object.
167 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
168
169 Returns:
170 A tuple of a gerrit object and a sanitized CL #.
171 """
172 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700173 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600174 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600175 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600176 if cl.startswith('*'):
177 cl = cl[1:]
178 else:
179 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700180 elif ':' in cl:
181 gob, cl = cl.split(':', 1)
182
183 if not gob in opts.gerrit:
184 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
185
186 return (opts.gerrit[gob], cl)
187
188
Mike Frysinger13f23a42013-05-13 17:32:01 -0400189def GetApprovalSummary(_opts, cls):
190 """Return a dict of the most important approvals"""
191 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700192 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
193 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
194 if not cats:
195 logging.warning('unknown gerrit approval type: %s', approver['type'])
196 continue
197 cat = cats[0].strip()
198 val = int(approver['value'])
199 if not cat in approvs:
200 # Ignore the extended categories in the summary view.
201 continue
202 elif approvs[cat] == '':
203 approvs[cat] = val
204 elif val < 0:
205 approvs[cat] = min(approvs[cat], val)
206 else:
207 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400208 return approvs
209
210
Mike Frysingera1b4b272017-04-05 16:11:00 -0400211def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400212 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400213 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400214 lims = {'url': 0, 'project': 0}
215
216 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400217
218 if opts.verbose:
219 status += '%s ' % (cl['status'],)
220 else:
221 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
222
Mike Frysinger13f23a42013-05-13 17:32:01 -0400223 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400224 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400225 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400226 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400227 functor = lambda x: x
228 elif approvs[cat] < 0:
229 functor = red
230 else:
231 functor = green
232 status += functor('%s:%2s ' % (cat, approvs[cat]))
233
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400234 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
235 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400236
237 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400238 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400239 functor = red if int(approver['value']) < 0 else green
240 n = functor('%2s' % approver['value'])
241 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
242 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500243 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400244
245
Mike Frysingera1b4b272017-04-05 16:11:00 -0400246def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400247 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400248 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600249 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400250 pfx = ''
251 # Special case internal Chrome GoB as that is what most devs use.
252 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600253 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
254 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400255 for cl in cls:
256 print('%s%s' % (pfx, cl['number']))
257
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400258 elif opts.json:
259 json.dump(cls, sys.stdout)
260
Mike Frysingera1b4b272017-04-05 16:11:00 -0400261 else:
262 if lims is None:
263 lims = limits(cls)
264
265 for cl in cls:
266 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
267
268
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400269def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700270 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800271 if opts.branch is not None:
272 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800273 if opts.project is not None:
274 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800275 if opts.topic is not None:
276 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800277
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400278 if helper is None:
279 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700280 return helper.Query(query, raw=raw, bypass_cache=False)
281
282
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400283def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700284 """Query gerrit and filter/clean up the results"""
285 ret = []
286
Mike Frysinger2cd56022017-01-12 20:56:27 -0500287 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400288 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400289 # Gerrit likes to return a stats record too.
290 if not 'project' in cl:
291 continue
292
293 # Strip off common leading names since the result is still
294 # unique over the whole tree.
295 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400296 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
297 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400298 if cl['project'].startswith('%s/' % pfx):
299 cl['project'] = cl['project'][len(pfx) + 1:]
300
Mike Frysinger479f1192017-09-14 22:36:30 -0400301 cl['url'] = uri_lib.ShortenUri(cl['url'])
302
Mike Frysinger13f23a42013-05-13 17:32:01 -0400303 ret.append(cl)
304
Mike Frysingerb62313a2017-06-30 16:38:58 -0400305 if opts.sort == 'unsorted':
306 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700307 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400308 key = lambda x: int(x[opts.sort])
309 else:
310 key = lambda x: x[opts.sort]
311 return sorted(ret, key=key)
312
313
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500314class _ActionSearchQuery(UserAction):
315 """Base class for actions that perform searches."""
316
317 @staticmethod
318 def init_subparser(parser):
319 """Add arguments to this action's subparser."""
320 parser.add_argument('--sort', default='number',
321 help='Key to sort on (number, project); use "unsorted" '
322 'to disable')
323 parser.add_argument('-b', '--branch',
324 help='Limit output to the specific branch')
325 parser.add_argument('-p', '--project',
326 help='Limit output to the specific project')
327 parser.add_argument('-t', '--topic',
328 help='Limit output to the specific topic')
329
330
331class ActionTodo(_ActionSearchQuery):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400332 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500333
334 COMMAND = 'todo'
335
336 @staticmethod
337 def __call__(opts):
338 """Implement the action."""
Mike Frysinger242d2922021-02-09 14:31:50 -0500339 cls = FilteredQuery(opts, 'attention:self')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500340 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400341
342
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500343class ActionSearch(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800344 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500345
346 COMMAND = 'search'
347
348 @staticmethod
349 def init_subparser(parser):
350 """Add arguments to this action's subparser."""
351 _ActionSearchQuery.init_subparser(parser)
352 parser.add_argument('query',
353 help='The search query')
354
355 @staticmethod
356 def __call__(opts):
357 """Implement the action."""
358 cls = FilteredQuery(opts, opts.query)
359 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400360
361
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500362class ActionMine(_ActionSearchQuery):
Mike Frysingera1db2c42014-06-15 00:42:48 -0700363 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500364
365 COMMAND = 'mine'
366
367 @staticmethod
368 def init_subparser(parser):
369 """Add arguments to this action's subparser."""
370 _ActionSearchQuery.init_subparser(parser)
371 parser.add_argument('--draft', default=False, action='store_true',
372 help='Show draft changes')
373
374 @staticmethod
375 def __call__(opts):
376 """Implement the action."""
377 if opts.draft:
378 rule = 'is:draft'
379 else:
380 rule = 'status:new'
381 cls = FilteredQuery(opts, 'owner:self %s' % (rule,))
382 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700383
384
Paul Hobbs89765232015-06-24 14:07:49 -0700385def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
386 """Runs breadth first search starting from the nodes in |to_visit|
387
388 Args:
389 to_visit: the starting nodes
390 children: a function which takes a node and returns the nodes adjacent to it
391 visited_key: a function for deduplicating node visits. Defaults to the
392 identity function (lambda x: x)
393
394 Returns:
395 A list of nodes which are reachable from any node in |to_visit| by calling
396 |children| any number of times.
397 """
398 to_visit = list(to_visit)
Mike Frysinger66ce4132019-07-17 22:52:52 -0400399 seen = set(visited_key(x) for x in to_visit)
Paul Hobbs89765232015-06-24 14:07:49 -0700400 for node in to_visit:
401 for child in children(node):
402 key = visited_key(child)
403 if key not in seen:
404 seen.add(key)
405 to_visit.append(child)
406 return to_visit
407
408
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500409class ActionDeps(_ActionSearchQuery):
Paul Hobbs89765232015-06-24 14:07:49 -0700410 """List CLs matching a query, and all transitive dependencies of those CLs"""
Paul Hobbs89765232015-06-24 14:07:49 -0700411
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500412 COMMAND = 'deps'
Paul Hobbs89765232015-06-24 14:07:49 -0700413
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500414 @staticmethod
415 def init_subparser(parser):
416 """Add arguments to this action's subparser."""
417 _ActionSearchQuery.init_subparser(parser)
418 parser.add_argument('query',
419 help='The search query')
420
421 def __call__(self, opts):
422 """Implement the action."""
423 cls = _Query(opts, opts.query, raw=False)
424
425 @memoize.Memoize
426 def _QueryChange(cl, helper=None):
427 return _Query(opts, cl, raw=False, helper=helper)
428
429 transitives = _BreadthFirstSearch(
Mike Nicholsa1414162021-04-22 20:07:22 +0000430 cls, functools.partial(self._Children, opts, _QueryChange),
Mike Frysingerdc407f52020-05-08 00:34:56 -0400431 visited_key=lambda cl: cl.PatchLink())
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500432
Mike Frysingerdc407f52020-05-08 00:34:56 -0400433 # This is a hack to avoid losing GoB host for each CL. The PrintCls
434 # function assumes the GoB host specified by the user is the only one
435 # that is ever used, but the deps command walks across hosts.
436 if opts.raw:
437 print('\n'.join(x.PatchLink() for x in transitives))
438 else:
439 transitives_raw = [cl.patch_dict for cl in transitives]
440 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500441
442 @staticmethod
443 def _ProcessDeps(opts, querier, cl, deps, required):
Mike Frysinger5726da92017-09-20 22:14:25 -0400444 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700445 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400446 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400447 if not dep.remote in opts.gerrit:
448 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
449 remote=dep.remote, print_cmd=opts.debug)
450 helper = opts.gerrit[dep.remote]
451
Paul Hobbs89765232015-06-24 14:07:49 -0700452 # TODO(phobbs) this should maybe catch network errors.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500453 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400454
455 # Handle empty results. If we found a commit that was pushed directly
456 # (e.g. a bot commit), then gerrit won't know about it.
457 if not changes:
458 if required:
459 logging.error('CL %s depends on %s which cannot be found',
460 cl, dep.ToGerritQueryText())
461 continue
462
463 # Our query might have matched more than one result. This can come up
464 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
465 # across multiple repos/branches. We blindly check all of them in the
466 # hopes that all open ones are what the user wants, but then again the
467 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
468 if len(changes) > 1:
469 logging.warning('CL %s has an ambiguous CQ dependency %s',
470 cl, dep.ToGerritQueryText())
471 for change in changes:
472 if change.status == 'NEW':
473 yield change
474
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500475 @classmethod
Mike Nicholsa1414162021-04-22 20:07:22 +0000476 def _Children(cls, opts, querier, cl):
Mike Frysinger7cbd88c2021-02-12 03:52:25 -0500477 """Yields the Gerrit dependencies of a patch"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500478 for change in cls._ProcessDeps(
479 opts, querier, cl, cl.GerritDependencies(), False):
Mike Frysinger5726da92017-09-20 22:14:25 -0400480 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700481
Paul Hobbs89765232015-06-24 14:07:49 -0700482
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500483class ActionInspect(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800484 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500485
486 COMMAND = 'inspect'
487
488 @staticmethod
489 def init_subparser(parser):
490 """Add arguments to this action's subparser."""
491 _ActionSearchQuery.init_subparser(parser)
492 parser.add_argument('cls', nargs='+', metavar='CL',
493 help='The CL(s) to update')
494
495 @staticmethod
496 def __call__(opts):
497 """Implement the action."""
498 cls = []
499 for arg in opts.cls:
500 helper, cl = GetGerrit(opts, arg)
501 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
502 if change:
503 cls.extend(change)
504 else:
505 logging.warning('no results found for CL %s', arg)
506 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400507
508
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500509class _ActionLabeler(UserAction):
510 """Base helper for setting labels."""
511
512 LABEL = None
513 VALUES = None
514
515 @classmethod
516 def init_subparser(cls, parser):
517 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500518 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
519 help='Optional message to include')
520 parser.add_argument('cls', nargs='+', metavar='CL',
521 help='The CL(s) to update')
522 parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
523 help='The label value; one of [%(choices)s]')
524
525 @classmethod
526 def __call__(cls, opts):
527 """Implement the action."""
528 # Convert user friendly command line option into a gerrit parameter.
529 def task(arg):
530 helper, cl = GetGerrit(opts, arg)
531 helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
532 dryrun=opts.dryrun, notify=opts.notify)
533 _run_parallel_tasks(task, *opts.cls)
534
535
536class ActionLabelAutoSubmit(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500537 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500538
539 COMMAND = 'label-as'
540 LABEL = 'Auto-Submit'
541 VALUES = ('0', '1')
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600542
543
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500544class ActionLabelCodeReview(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500545 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500546
547 COMMAND = 'label-cr'
548 LABEL = 'Code-Review'
549 VALUES = ('-2', '-1', '0', '1', '2')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400550
551
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500552class ActionLabelVerified(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500553 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500554
555 COMMAND = 'label-v'
556 LABEL = 'Verified'
557 VALUES = ('-1', '0', '1')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400558
559
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500560class ActionLabelCommitQueue(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500561 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500562
563 COMMAND = 'label-cq'
564 LABEL = 'Commit-Queue'
565 VALUES = ('0', '1', '2')
Mike Frysinger15b23e42014-12-05 17:00:05 -0500566
567
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500568class _ActionSimpleParallelCLs(UserAction):
569 """Base helper for actions that only accept CLs."""
570
571 @staticmethod
572 def init_subparser(parser):
573 """Add arguments to this action's subparser."""
574 parser.add_argument('cls', nargs='+', metavar='CL',
575 help='The CL(s) to update')
576
577 def __call__(self, opts):
578 """Implement the action."""
579 def task(arg):
580 helper, cl = GetGerrit(opts, arg)
581 self._process_one(helper, cl, opts)
582 _run_parallel_tasks(task, *opts.cls)
583
584
585class ActionSubmit(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800586 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500587
588 COMMAND = 'submit'
589
590 @staticmethod
591 def _process_one(helper, cl, opts):
592 """Use |helper| to process the single |cl|."""
Mike Frysinger8674a112021-02-09 14:44:17 -0500593 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400594
595
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500596class ActionAbandon(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800597 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500598
599 COMMAND = 'abandon'
600
601 @staticmethod
Mike Frysinger3af378b2021-03-12 01:34:04 -0500602 def init_subparser(parser):
603 """Add arguments to this action's subparser."""
604 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
605 help='Include a message')
606 _ActionSimpleParallelCLs.init_subparser(parser)
607
608 @staticmethod
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500609 def _process_one(helper, cl, opts):
610 """Use |helper| to process the single |cl|."""
Mike Frysinger3af378b2021-03-12 01:34:04 -0500611 helper.AbandonChange(cl, msg=opts.msg, dryrun=opts.dryrun,
612 notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400613
614
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500615class ActionRestore(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800616 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500617
618 COMMAND = 'restore'
619
620 @staticmethod
621 def _process_one(helper, cl, opts):
622 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700623 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400624
625
Tomasz Figa54d70992021-01-20 13:48:59 +0900626class ActionWorkInProgress(_ActionSimpleParallelCLs):
627 """Mark CLs as work in progress"""
628
629 COMMAND = 'wip'
630
631 @staticmethod
632 def _process_one(helper, cl, opts):
633 """Use |helper| to process the single |cl|."""
634 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
635
636
637class ActionReadyForReview(_ActionSimpleParallelCLs):
638 """Mark CLs as ready for review"""
639
640 COMMAND = 'ready'
641
642 @staticmethod
643 def _process_one(helper, cl, opts):
644 """Use |helper| to process the single |cl|."""
645 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
646
647
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500648class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800649 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700650
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500651 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700652
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500653 @staticmethod
654 def init_subparser(parser):
655 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500656 parser.add_argument('cl', metavar='CL',
657 help='The CL to update')
658 parser.add_argument('reviewers', nargs='+',
659 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700660
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500661 @staticmethod
662 def __call__(opts):
663 """Implement the action."""
664 # Allow for optional leading '~'.
665 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
666 add_list, remove_list, invalid_list = [], [], []
667
668 for email in opts.reviewers:
669 if not email_validator.match(email):
670 invalid_list.append(email)
671 elif email[0] == '~':
672 remove_list.append(email[1:])
673 else:
674 add_list.append(email)
675
676 if invalid_list:
677 cros_build_lib.Die(
678 'Invalid email address(es): %s' % ', '.join(invalid_list))
679
680 if add_list or remove_list:
681 helper, cl = GetGerrit(opts, opts.cl)
682 helper.SetReviewers(cl, add=add_list, remove=remove_list,
683 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700684
685
Mike Frysinger62178ae2020-03-20 01:37:43 -0400686class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800687 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500688
689 COMMAND = 'message'
690
691 @staticmethod
692 def init_subparser(parser):
693 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400694 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500695 parser.add_argument('message',
696 help='The message to post')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500697
698 @staticmethod
699 def _process_one(helper, cl, opts):
700 """Use |helper| to process the single |cl|."""
701 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530702
703
Mike Frysinger62178ae2020-03-20 01:37:43 -0400704class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800705 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500706
707 COMMAND = 'topic'
708
709 @staticmethod
710 def init_subparser(parser):
711 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400712 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500713 parser.add_argument('topic',
714 help='The topic to set')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500715
716 @staticmethod
717 def _process_one(helper, cl, opts):
718 """Use |helper| to process the single |cl|."""
719 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800720
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800721
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500722class ActionPrivate(_ActionSimpleParallelCLs):
723 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700724
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500725 COMMAND = 'private'
726
727 @staticmethod
728 def _process_one(helper, cl, opts):
729 """Use |helper| to process the single |cl|."""
730 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700731
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800732
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500733class ActionPublic(_ActionSimpleParallelCLs):
734 """Mark CLs public"""
735
736 COMMAND = 'public'
737
738 @staticmethod
739 def _process_one(helper, cl, opts):
740 """Use |helper| to process the single |cl|."""
741 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
742
743
744class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800745 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500746
747 COMMAND = 'hashtags'
748
749 @staticmethod
750 def init_subparser(parser):
751 """Add arguments to this action's subparser."""
752 parser.add_argument('cl', metavar='CL',
753 help='The CL to update')
754 parser.add_argument('hashtags', nargs='+',
755 help='The hashtags to add/remove')
756
757 @staticmethod
758 def __call__(opts):
759 """Implement the action."""
760 add = []
761 remove = []
762 for hashtag in opts.hashtags:
763 if hashtag.startswith('~'):
764 remove.append(hashtag[1:])
765 else:
766 add.append(hashtag)
767 helper, cl = GetGerrit(opts, opts.cl)
768 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800769
770
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500771class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800772 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500773
774 COMMAND = 'deletedraft'
775
776 @staticmethod
777 def _process_one(helper, cl, opts):
778 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700779 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800780
781
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500782class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500783 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500784
785 COMMAND = 'reviewed'
786
787 @staticmethod
788 def _process_one(helper, cl, opts):
789 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500790 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500791
792
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500793class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500794 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500795
796 COMMAND = 'unreviewed'
797
798 @staticmethod
799 def _process_one(helper, cl, opts):
800 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500801 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500802
803
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500804class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500805 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500806
807 COMMAND = 'ignore'
808
809 @staticmethod
810 def _process_one(helper, cl, opts):
811 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500812 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500813
814
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500815class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500816 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500817
818 COMMAND = 'unignore'
819
820 @staticmethod
821 def _process_one(helper, cl, opts):
822 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500823 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500824
825
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400826class ActionCherryPick(UserAction):
827 """Cherry pick CLs to branches."""
828
829 COMMAND = 'cherry-pick'
830
831 @staticmethod
832 def init_subparser(parser):
833 """Add arguments to this action's subparser."""
834 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
835 parser.add_argument('--rev', '--revision', default='current',
836 help='A specific revision or patchset')
837 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
838 help='Include a message')
839 parser.add_argument('--branches', '--branch', '--br', action='split_extend',
840 default=[], required=True,
841 help='The destination branches')
842 parser.add_argument('cls', nargs='+', metavar='CL',
843 help='The CLs to cherry-pick')
844
845 @staticmethod
846 def __call__(opts):
847 """Implement the action."""
848 # Process branches in parallel, but CLs in serial in case of CL stacks.
849 def task(branch):
850 for arg in opts.cls:
851 helper, cl = GetGerrit(opts, arg)
852 ret = helper.CherryPick(cl, branch, rev=opts.rev, msg=opts.msg,
Mike Frysinger8674a112021-02-09 14:44:17 -0500853 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400854 logging.debug('Response: %s', ret)
855 if opts.raw:
856 print(ret['_number'])
857 else:
858 uri = f'https://{helper.host}/c/{ret["_number"]}'
859 print(uri_lib.ShortenUri(uri))
860
861 _run_parallel_tasks(task, *opts.branches)
862
863
Mike Frysinger8037f752020-02-29 20:47:09 -0500864class ActionReview(_ActionSimpleParallelCLs):
865 """Review CLs with multiple settings
866
867 The label option supports extended/multiple syntax for easy use. The --label
868 option may be specified multiple times (as settings are merges), and multiple
869 labels are allowed in a single argument. Each label has the form:
870 <long or short name><=+-><value>
871
872 Common arguments:
873 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
874 'V+1 CQ+2'
875 'AS=1 V=1'
876 """
877
878 COMMAND = 'review'
879
880 class _SetLabel(argparse.Action):
881 """Argparse action for setting labels."""
882
883 LABEL_MAP = {
884 'AS': 'Auto-Submit',
885 'CQ': 'Commit-Queue',
886 'CR': 'Code-Review',
887 'V': 'Verified',
888 }
889
890 def __call__(self, parser, namespace, values, option_string=None):
891 labels = getattr(namespace, self.dest)
892 for request in values.split():
893 if '=' in request:
894 # Handle Verified=1 form.
895 short, value = request.split('=', 1)
896 elif '+' in request:
897 # Handle Verified+1 form.
898 short, value = request.split('+', 1)
899 elif '-' in request:
900 # Handle Verified-1 form.
901 short, value = request.split('-', 1)
902 value = '-%s' % (value,)
903 else:
904 parser.error('Invalid label setting "%s". Must be Commit-Queue=1 or '
905 'CQ+1 or CR-1.' % (request,))
906
907 # Convert possible short label names like "V" to "Verified".
908 label = self.LABEL_MAP.get(short)
909 if not label:
910 label = short
911
912 # We allow existing label requests to be overridden.
913 labels[label] = value
914
915 @classmethod
916 def init_subparser(cls, parser):
917 """Add arguments to this action's subparser."""
918 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
919 help='Include a message')
920 parser.add_argument('-l', '--label', dest='labels',
921 action=cls._SetLabel, default={},
922 help='Set a label with a value')
923 parser.add_argument('--ready', default=None, action='store_true',
924 help='Set CL status to ready-for-review')
925 parser.add_argument('--wip', default=None, action='store_true',
926 help='Set CL status to WIP')
927 parser.add_argument('--reviewers', '--re', action='append', default=[],
928 help='Add reviewers')
929 parser.add_argument('--cc', action='append', default=[],
930 help='Add people to CC')
931 _ActionSimpleParallelCLs.init_subparser(parser)
932
933 @staticmethod
934 def _process_one(helper, cl, opts):
935 """Use |helper| to process the single |cl|."""
936 helper.SetReview(cl, msg=opts.msg, labels=opts.labels, dryrun=opts.dryrun,
937 notify=opts.notify, reviewers=opts.reviewers, cc=opts.cc,
938 ready=opts.ready, wip=opts.wip)
939
940
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500941class ActionAccount(_ActionSimpleParallelCLs):
942 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500943
944 COMMAND = 'account'
945
946 @staticmethod
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500947 def init_subparser(parser):
948 """Add arguments to this action's subparser."""
949 parser.add_argument('accounts', nargs='*', default=['self'],
950 help='The accounts to query')
951
952 @classmethod
953 def __call__(cls, opts):
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500954 """Implement the action."""
955 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500956
957 def print_one(header, data):
958 print(f'### {header}')
959 print(pformat.json(data, compact=opts.json).rstrip())
960
961 def task(arg):
962 detail = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/detail')
963 if not detail:
964 print(f'{arg}: account not found')
965 else:
966 print_one('detail', detail)
967 for field in ('groups', 'capabilities', 'preferences', 'sshkeys',
968 'gpgkeys'):
969 data = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/{field}')
970 print_one(field, data)
971
972 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800973
974
Mike Frysinger2295d792021-03-08 15:55:23 -0500975class ActionConfig(UserAction):
976 """Manage the gerrit tool's own config file
977
978 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
979 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
980
981 # Set up subcommand aliases.
982 [alias]
983 common-search = search 'is:open project:something/i/care/about'
984 """
985
986 COMMAND = 'config'
987
988 @staticmethod
989 def __call__(opts):
990 """Implement the action."""
991 # For now, this is a place holder for raising visibility for the config file
992 # and its associated help text documentation.
993 opts.parser.parse_args(['config', '--help'])
994
995
Mike Frysingere5450602021-03-08 15:34:17 -0500996class ActionHelp(UserAction):
997 """An alias to --help for CLI symmetry"""
998
999 COMMAND = 'help'
1000
1001 @staticmethod
1002 def init_subparser(parser):
1003 """Add arguments to this action's subparser."""
1004 parser.add_argument('command', nargs='?',
1005 help='The command to display.')
1006
1007 @staticmethod
1008 def __call__(opts):
1009 """Implement the action."""
1010 # Show global help.
1011 if not opts.command:
1012 opts.parser.print_help()
1013 return
1014
1015 opts.parser.parse_args([opts.command, '--help'])
1016
1017
Mike Frysinger484e2f82020-03-20 01:41:10 -04001018class ActionHelpAll(UserAction):
1019 """Show all actions help output at once."""
1020
1021 COMMAND = 'help-all'
1022
1023 @staticmethod
1024 def __call__(opts):
1025 """Implement the action."""
1026 first = True
1027 for action in _GetActions():
1028 if first:
1029 first = False
1030 else:
1031 print('\n\n')
1032
1033 try:
1034 opts.parser.parse_args([action, '--help'])
1035 except SystemExit:
1036 pass
1037
1038
Mike Frysinger65fc8632020-02-06 18:11:12 -05001039@memoize.Memoize
1040def _GetActions():
1041 """Get all the possible actions we support.
1042
1043 Returns:
1044 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1045 function that implements that command (e.g. UserActFoo).
1046 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001047 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
1048
1049 actions = {}
1050 for cls in globals().values():
1051 if (not inspect.isclass(cls) or
1052 not issubclass(cls, UserAction) or
1053 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -05001054 continue
1055
Mike Frysinger65fc8632020-02-06 18:11:12 -05001056 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001057 cmd = cls.COMMAND
1058 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1059 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001060
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001061 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001062
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001063 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001064
1065
Harry Cutts26076b32019-02-26 15:01:29 -08001066def _GetActionUsages():
1067 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -05001068 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001069
Mike Frysinger65fc8632020-02-06 18:11:12 -05001070 cmds = list(actions.keys())
1071 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -08001072 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001073 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001074
Harry Cutts26076b32019-02-26 15:01:29 -08001075 cmd_indent = len(max(cmds, key=len))
1076 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001077 return '\n'.join(
1078 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
1079 for cmd, usage, doc in zip(cmds, usages, docs)
1080 )
Harry Cutts26076b32019-02-26 15:01:29 -08001081
1082
Mike Frysinger2295d792021-03-08 15:55:23 -05001083def _AddCommonOptions(parser, subparser):
1084 """Add options that should work before & after the subcommand.
1085
1086 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1087 """
1088 parser.add_common_argument_to_group(
1089 subparser, '--ne', '--no-emails', dest='notify',
1090 default='ALL', action='store_const', const='NONE',
1091 help='Do not send e-mail notifications')
1092 parser.add_common_argument_to_group(
1093 subparser, '-n', '--dry-run', dest='dryrun',
1094 default=False, action='store_true',
1095 help='Show what would be done, but do not make changes')
1096
1097
1098def GetBaseParser() -> commandline.ArgumentParser:
1099 """Returns the common parser (i.e. no subparsers added)."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001100 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001101There is no support for doing line-by-line code review via the command line.
1102This helps you manage various bits and CL status.
1103
Mike Frysingera1db2c42014-06-15 00:42:48 -07001104For general Gerrit documentation, see:
1105 https://gerrit-review.googlesource.com/Documentation/
1106The Searching Changes page covers the search query syntax:
1107 https://gerrit-review.googlesource.com/Documentation/user-search.html
1108
Mike Frysinger13f23a42013-05-13 17:32:01 -04001109Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001110 $ gerrit todo # List all the CLs that await your review.
1111 $ gerrit mine # List all of your open CLs.
1112 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1113 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1114 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001115 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
111628123.
1117 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1118CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001119Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001120 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1121with Commit-Queue=1.
1122 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1123CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001124 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001125
Harry Cutts26076b32019-02-26 15:01:29 -08001126Actions:
1127"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001128 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001129
Alex Klein2ab29cc2018-07-19 12:01:00 -06001130 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -04001131 parser = commandline.ArgumentParser(
1132 description=description, default_log_level='notice')
Mike Frysinger8674a112021-02-09 14:44:17 -05001133
1134 group = parser.add_argument_group('Server options')
1135 group.add_argument('-i', '--internal', dest='gob', action='store_const',
1136 default=site_params.EXTERNAL_GOB_INSTANCE,
1137 const=site_params.INTERNAL_GOB_INSTANCE,
1138 help='Query internal Chrome Gerrit instance')
1139 group.add_argument('-g', '--gob',
1140 default=site_params.EXTERNAL_GOB_INSTANCE,
1141 help='Gerrit (on borg) instance to query (default: %s)' %
1142 (site_params.EXTERNAL_GOB_INSTANCE))
1143
Mike Frysinger8674a112021-02-09 14:44:17 -05001144 group = parser.add_argument_group('CL options')
Mike Frysinger2295d792021-03-08 15:55:23 -05001145 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001146
Mike Frysingerf70bdc72014-06-15 00:44:06 -07001147 parser.add_argument('--raw', default=False, action='store_true',
1148 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -04001149 parser.add_argument('--json', default=False, action='store_true',
1150 help='Return results in JSON (suitable for scripting)')
Mike Frysinger2295d792021-03-08 15:55:23 -05001151 return parser
1152
1153
1154def GetParser(parser: commandline.ArgumentParser = None) -> (
1155 commandline.ArgumentParser):
1156 """Returns the full parser to use for this module."""
1157 if parser is None:
1158 parser = GetBaseParser()
1159
1160 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001161
1162 # Subparsers are required by default under Python 2. Python 3 changed to
1163 # not required, but didn't include a required option until 3.7. Setting
1164 # the required member works in all versions (and setting dest name).
1165 subparsers = parser.add_subparsers(dest='action')
1166 subparsers.required = True
1167 for cmd, cls in actions.items():
1168 # Format the full docstring by removing the file level indentation.
1169 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
1170 subparser = subparsers.add_parser(cmd, description=description)
Mike Frysinger2295d792021-03-08 15:55:23 -05001171 _AddCommonOptions(parser, subparser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001172 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001173
1174 return parser
1175
1176
1177def main(argv):
Mike Frysinger2295d792021-03-08 15:55:23 -05001178 base_parser = GetBaseParser()
1179 opts, subargs = base_parser.parse_known_args(argv)
1180
1181 config = Config()
1182 if subargs:
1183 # If the action is an alias to an expanded value, we need to mutate the argv
1184 # and reparse things.
1185 action = config.expand_alias(subargs[0])
1186 if action != subargs[0]:
1187 pos = argv.index(subargs[0])
1188 argv = argv[:pos] + action + argv[pos + 1:]
1189
1190 parser = GetParser(parser=base_parser)
Mike Frysingerddf86eb2014-02-07 22:51:41 -05001191 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001192
Mike Frysinger484e2f82020-03-20 01:41:10 -04001193 # In case the action wants to throw a parser error.
1194 opts.parser = parser
1195
Mike Frysinger88f27292014-06-17 09:40:45 -07001196 # A cache of gerrit helpers we'll load on demand.
1197 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001198
Mike Frysinger88f27292014-06-17 09:40:45 -07001199 opts.Freeze()
1200
Mike Frysinger27e21b72018-07-12 14:20:21 -04001201 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001202 global COLOR
1203 COLOR = terminal.Color(enabled=opts.color)
1204
Mike Frysinger13f23a42013-05-13 17:32:01 -04001205 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001206 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001207 obj = actions[opts.action]()
Mike Frysinger65fc8632020-02-06 18:11:12 -05001208 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001209 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001210 except (cros_build_lib.RunCommandError, gerrit.GerritException,
1211 gob_util.GOBError) as e:
1212 cros_build_lib.Die(e)