blob: d3790add8818be3b83dee423b87afc2339987348 [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 Frysinger65fc8632020-02-06 18:11:12 -050015import collections
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
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070019import re
Mike Frysinger87c74ce2017-04-04 16:12:31 -040020import sys
Mike Frysinger13f23a42013-05-13 17:32:01 -040021
Aviv Keshetb7519e12016-10-04 00:50:00 -070022from chromite.lib import config_lib
23from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040024from chromite.lib import commandline
25from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070026from chromite.lib import cros_logging as logging
Mike Frysinger13f23a42013-05-13 17:32:01 -040027from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050028from chromite.lib import gob_util
Mike Frysinger254f33f2019-12-11 13:54:29 -050029from chromite.lib import parallel
Mike Frysinger7f2018d2021-02-04 00:10:58 -050030from chromite.lib import pformat
Mike Frysinger13f23a42013-05-13 17:32:01 -040031from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040032from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060033from chromite.utils import memoize
Mike Frysinger13f23a42013-05-13 17:32:01 -040034
35
Mike Frysinger1c76d4c2020-02-08 23:35:29 -050036assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
37
38
Mike Frysingerc7796cf2020-02-06 23:55:15 -050039class UserAction(object):
40 """Base class for all custom user actions."""
41
42 # The name of the command the user types in.
43 COMMAND = None
44
45 @staticmethod
46 def init_subparser(parser):
47 """Add arguments to this action's subparser."""
48
49 @staticmethod
50 def __call__(opts):
51 """Implement the action."""
Mike Frysinger62178ae2020-03-20 01:37:43 -040052 raise RuntimeError('Internal error: action missing __call__ implementation')
Mike Frysinger108eda22018-06-06 18:45:12 -040053
54
Mike Frysinger254f33f2019-12-11 13:54:29 -050055# How many connections we'll use in parallel. We don't want this to be too high
56# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
57# seems to be good enough for users.
58CONNECTION_LIMIT = 10
59
60
Mike Frysinger031ad0b2013-05-14 18:15:34 -040061COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040062
63# Map the internal names to the ones we normally show on the web ui.
64GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080065 'COMR': ['CQ', 'Commit Queue ',],
66 'CRVW': ['CR', 'Code Review ',],
67 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080068 'VRIF': ['V ', 'Verified ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060069 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040070}
71
72# Order is important -- matches the web ui. This also controls the short
73# entries that we summarize in non-verbose mode.
74GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
75
Mike Frysinger4aea5dc2019-07-17 13:39:56 -040076# Shorter strings for CL status messages.
77GERRIT_SUMMARY_MAP = {
78 'ABANDONED': 'ABD',
79 'MERGED': 'MRG',
80 'NEW': 'NEW',
81 'WIP': 'WIP',
82}
83
Mike Frysinger13f23a42013-05-13 17:32:01 -040084
85def red(s):
86 return COLOR.Color(terminal.Color.RED, s)
87
88
89def green(s):
90 return COLOR.Color(terminal.Color.GREEN, s)
91
92
93def blue(s):
94 return COLOR.Color(terminal.Color.BLUE, s)
95
96
Mike Frysinger254f33f2019-12-11 13:54:29 -050097def _run_parallel_tasks(task, *args):
98 """Small wrapper around BackgroundTaskRunner to enforce job count."""
99 with parallel.BackgroundTaskRunner(task, processes=CONNECTION_LIMIT) as q:
100 for arg in args:
101 q.put([arg])
102
103
Mike Frysinger13f23a42013-05-13 17:32:01 -0400104def limits(cls):
105 """Given a dict of fields, calculate the longest string lengths
106
107 This allows you to easily format the output of many results so that the
108 various cols all line up correctly.
109 """
110 lims = {}
111 for cl in cls:
112 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -0400113 # Use %s rather than str() to avoid codec issues.
114 # We also do this so we can format integers.
115 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400116 return lims
117
118
Mike Frysinger88f27292014-06-17 09:40:45 -0700119# TODO: This func really needs to be merged into the core gerrit logic.
120def GetGerrit(opts, cl=None):
121 """Auto pick the right gerrit instance based on the |cl|
122
123 Args:
124 opts: The general options object.
125 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
126
127 Returns:
128 A tuple of a gerrit object and a sanitized CL #.
129 """
130 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700131 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600132 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600133 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600134 if cl.startswith('*'):
135 cl = cl[1:]
136 else:
137 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700138 elif ':' in cl:
139 gob, cl = cl.split(':', 1)
140
141 if not gob in opts.gerrit:
142 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
143
144 return (opts.gerrit[gob], cl)
145
146
Mike Frysinger13f23a42013-05-13 17:32:01 -0400147def GetApprovalSummary(_opts, cls):
148 """Return a dict of the most important approvals"""
149 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700150 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
151 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
152 if not cats:
153 logging.warning('unknown gerrit approval type: %s', approver['type'])
154 continue
155 cat = cats[0].strip()
156 val = int(approver['value'])
157 if not cat in approvs:
158 # Ignore the extended categories in the summary view.
159 continue
160 elif approvs[cat] == '':
161 approvs[cat] = val
162 elif val < 0:
163 approvs[cat] = min(approvs[cat], val)
164 else:
165 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400166 return approvs
167
168
Mike Frysingera1b4b272017-04-05 16:11:00 -0400169def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400170 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400171 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400172 lims = {'url': 0, 'project': 0}
173
174 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400175
176 if opts.verbose:
177 status += '%s ' % (cl['status'],)
178 else:
179 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
180
Mike Frysinger13f23a42013-05-13 17:32:01 -0400181 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400182 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400183 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400184 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400185 functor = lambda x: x
186 elif approvs[cat] < 0:
187 functor = red
188 else:
189 functor = green
190 status += functor('%s:%2s ' % (cat, approvs[cat]))
191
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400192 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
193 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400194
195 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400196 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400197 functor = red if int(approver['value']) < 0 else green
198 n = functor('%2s' % approver['value'])
199 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
200 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500201 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400202
203
Mike Frysingera1b4b272017-04-05 16:11:00 -0400204def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400205 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400206 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600207 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400208 pfx = ''
209 # Special case internal Chrome GoB as that is what most devs use.
210 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600211 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
212 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400213 for cl in cls:
214 print('%s%s' % (pfx, cl['number']))
215
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400216 elif opts.json:
217 json.dump(cls, sys.stdout)
218
Mike Frysingera1b4b272017-04-05 16:11:00 -0400219 else:
220 if lims is None:
221 lims = limits(cls)
222
223 for cl in cls:
224 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
225
226
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400227def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700228 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800229 if opts.branch is not None:
230 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800231 if opts.project is not None:
232 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800233 if opts.topic is not None:
234 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800235
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400236 if helper is None:
237 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700238 return helper.Query(query, raw=raw, bypass_cache=False)
239
240
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400241def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700242 """Query gerrit and filter/clean up the results"""
243 ret = []
244
Mike Frysinger2cd56022017-01-12 20:56:27 -0500245 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400246 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400247 # Gerrit likes to return a stats record too.
248 if not 'project' in cl:
249 continue
250
251 # Strip off common leading names since the result is still
252 # unique over the whole tree.
253 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400254 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
255 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400256 if cl['project'].startswith('%s/' % pfx):
257 cl['project'] = cl['project'][len(pfx) + 1:]
258
Mike Frysinger479f1192017-09-14 22:36:30 -0400259 cl['url'] = uri_lib.ShortenUri(cl['url'])
260
Mike Frysinger13f23a42013-05-13 17:32:01 -0400261 ret.append(cl)
262
Mike Frysingerb62313a2017-06-30 16:38:58 -0400263 if opts.sort == 'unsorted':
264 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700265 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400266 key = lambda x: int(x[opts.sort])
267 else:
268 key = lambda x: x[opts.sort]
269 return sorted(ret, key=key)
270
271
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500272class _ActionSearchQuery(UserAction):
273 """Base class for actions that perform searches."""
274
275 @staticmethod
276 def init_subparser(parser):
277 """Add arguments to this action's subparser."""
278 parser.add_argument('--sort', default='number',
279 help='Key to sort on (number, project); use "unsorted" '
280 'to disable')
281 parser.add_argument('-b', '--branch',
282 help='Limit output to the specific branch')
283 parser.add_argument('-p', '--project',
284 help='Limit output to the specific project')
285 parser.add_argument('-t', '--topic',
286 help='Limit output to the specific topic')
287
288
289class ActionTodo(_ActionSearchQuery):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400290 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500291
292 COMMAND = 'todo'
293
294 @staticmethod
295 def __call__(opts):
296 """Implement the action."""
Mike Frysinger242d2922021-02-09 14:31:50 -0500297 cls = FilteredQuery(opts, 'attention:self')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500298 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400299
300
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500301class ActionSearch(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800302 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500303
304 COMMAND = 'search'
305
306 @staticmethod
307 def init_subparser(parser):
308 """Add arguments to this action's subparser."""
309 _ActionSearchQuery.init_subparser(parser)
310 parser.add_argument('query',
311 help='The search query')
312
313 @staticmethod
314 def __call__(opts):
315 """Implement the action."""
316 cls = FilteredQuery(opts, opts.query)
317 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400318
319
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500320class ActionMine(_ActionSearchQuery):
Mike Frysingera1db2c42014-06-15 00:42:48 -0700321 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500322
323 COMMAND = 'mine'
324
325 @staticmethod
326 def init_subparser(parser):
327 """Add arguments to this action's subparser."""
328 _ActionSearchQuery.init_subparser(parser)
329 parser.add_argument('--draft', default=False, action='store_true',
330 help='Show draft changes')
331
332 @staticmethod
333 def __call__(opts):
334 """Implement the action."""
335 if opts.draft:
336 rule = 'is:draft'
337 else:
338 rule = 'status:new'
339 cls = FilteredQuery(opts, 'owner:self %s' % (rule,))
340 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700341
342
Paul Hobbs89765232015-06-24 14:07:49 -0700343def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
344 """Runs breadth first search starting from the nodes in |to_visit|
345
346 Args:
347 to_visit: the starting nodes
348 children: a function which takes a node and returns the nodes adjacent to it
349 visited_key: a function for deduplicating node visits. Defaults to the
350 identity function (lambda x: x)
351
352 Returns:
353 A list of nodes which are reachable from any node in |to_visit| by calling
354 |children| any number of times.
355 """
356 to_visit = list(to_visit)
Mike Frysinger66ce4132019-07-17 22:52:52 -0400357 seen = set(visited_key(x) for x in to_visit)
Paul Hobbs89765232015-06-24 14:07:49 -0700358 for node in to_visit:
359 for child in children(node):
360 key = visited_key(child)
361 if key not in seen:
362 seen.add(key)
363 to_visit.append(child)
364 return to_visit
365
366
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500367class ActionDeps(_ActionSearchQuery):
Paul Hobbs89765232015-06-24 14:07:49 -0700368 """List CLs matching a query, and all transitive dependencies of those CLs"""
Paul Hobbs89765232015-06-24 14:07:49 -0700369
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500370 COMMAND = 'deps'
Paul Hobbs89765232015-06-24 14:07:49 -0700371
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500372 @staticmethod
373 def init_subparser(parser):
374 """Add arguments to this action's subparser."""
375 _ActionSearchQuery.init_subparser(parser)
376 parser.add_argument('query',
377 help='The search query')
378
379 def __call__(self, opts):
380 """Implement the action."""
381 cls = _Query(opts, opts.query, raw=False)
382
383 @memoize.Memoize
384 def _QueryChange(cl, helper=None):
385 return _Query(opts, cl, raw=False, helper=helper)
386
387 transitives = _BreadthFirstSearch(
388 cls, functools.partial(self._Children, opts, _QueryChange),
Mike Frysingerdc407f52020-05-08 00:34:56 -0400389 visited_key=lambda cl: cl.PatchLink())
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500390
Mike Frysingerdc407f52020-05-08 00:34:56 -0400391 # This is a hack to avoid losing GoB host for each CL. The PrintCls
392 # function assumes the GoB host specified by the user is the only one
393 # that is ever used, but the deps command walks across hosts.
394 if opts.raw:
395 print('\n'.join(x.PatchLink() for x in transitives))
396 else:
397 transitives_raw = [cl.patch_dict for cl in transitives]
398 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500399
400 @staticmethod
401 def _ProcessDeps(opts, querier, cl, deps, required):
Mike Frysinger5726da92017-09-20 22:14:25 -0400402 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700403 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400404 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400405 if not dep.remote in opts.gerrit:
406 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
407 remote=dep.remote, print_cmd=opts.debug)
408 helper = opts.gerrit[dep.remote]
409
Paul Hobbs89765232015-06-24 14:07:49 -0700410 # TODO(phobbs) this should maybe catch network errors.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500411 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400412
413 # Handle empty results. If we found a commit that was pushed directly
414 # (e.g. a bot commit), then gerrit won't know about it.
415 if not changes:
416 if required:
417 logging.error('CL %s depends on %s which cannot be found',
418 cl, dep.ToGerritQueryText())
419 continue
420
421 # Our query might have matched more than one result. This can come up
422 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
423 # across multiple repos/branches. We blindly check all of them in the
424 # hopes that all open ones are what the user wants, but then again the
425 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
426 if len(changes) > 1:
427 logging.warning('CL %s has an ambiguous CQ dependency %s',
428 cl, dep.ToGerritQueryText())
429 for change in changes:
430 if change.status == 'NEW':
431 yield change
432
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500433 @classmethod
434 def _Children(cls, opts, querier, cl):
Mike Frysinger5726da92017-09-20 22:14:25 -0400435 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500436 for change in cls._ProcessDeps(
437 opts, querier, cl, cl.PaladinDependencies(None), True):
Mike Frysinger5726da92017-09-20 22:14:25 -0400438 yield change
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500439 for change in cls._ProcessDeps(
440 opts, querier, cl, cl.GerritDependencies(), False):
Mike Frysinger5726da92017-09-20 22:14:25 -0400441 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700442
Paul Hobbs89765232015-06-24 14:07:49 -0700443
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500444class ActionInspect(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800445 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500446
447 COMMAND = 'inspect'
448
449 @staticmethod
450 def init_subparser(parser):
451 """Add arguments to this action's subparser."""
452 _ActionSearchQuery.init_subparser(parser)
453 parser.add_argument('cls', nargs='+', metavar='CL',
454 help='The CL(s) to update')
455
456 @staticmethod
457 def __call__(opts):
458 """Implement the action."""
459 cls = []
460 for arg in opts.cls:
461 helper, cl = GetGerrit(opts, arg)
462 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
463 if change:
464 cls.extend(change)
465 else:
466 logging.warning('no results found for CL %s', arg)
467 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400468
469
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500470class _ActionLabeler(UserAction):
471 """Base helper for setting labels."""
472
473 LABEL = None
474 VALUES = None
475
476 @classmethod
477 def init_subparser(cls, parser):
478 """Add arguments to this action's subparser."""
479 parser.add_argument('--ne', '--no-emails', dest='notify',
480 default='ALL', action='store_const', const='NONE',
481 help='Do not send e-mail notifications')
482 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
483 help='Optional message to include')
484 parser.add_argument('cls', nargs='+', metavar='CL',
485 help='The CL(s) to update')
486 parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
487 help='The label value; one of [%(choices)s]')
488
489 @classmethod
490 def __call__(cls, opts):
491 """Implement the action."""
492 # Convert user friendly command line option into a gerrit parameter.
493 def task(arg):
494 helper, cl = GetGerrit(opts, arg)
495 helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
496 dryrun=opts.dryrun, notify=opts.notify)
497 _run_parallel_tasks(task, *opts.cls)
498
499
500class ActionLabelAutoSubmit(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500501 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500502
503 COMMAND = 'label-as'
504 LABEL = 'Auto-Submit'
505 VALUES = ('0', '1')
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600506
507
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500508class ActionLabelCodeReview(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500509 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500510
511 COMMAND = 'label-cr'
512 LABEL = 'Code-Review'
513 VALUES = ('-2', '-1', '0', '1', '2')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400514
515
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500516class ActionLabelVerified(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500517 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500518
519 COMMAND = 'label-v'
520 LABEL = 'Verified'
521 VALUES = ('-1', '0', '1')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400522
523
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500524class ActionLabelCommitQueue(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500525 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500526
527 COMMAND = 'label-cq'
528 LABEL = 'Commit-Queue'
529 VALUES = ('0', '1', '2')
Mike Frysinger15b23e42014-12-05 17:00:05 -0500530
531
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500532class _ActionSimpleParallelCLs(UserAction):
533 """Base helper for actions that only accept CLs."""
534
535 @staticmethod
536 def init_subparser(parser):
537 """Add arguments to this action's subparser."""
538 parser.add_argument('cls', nargs='+', metavar='CL',
539 help='The CL(s) to update')
540
541 def __call__(self, opts):
542 """Implement the action."""
543 def task(arg):
544 helper, cl = GetGerrit(opts, arg)
545 self._process_one(helper, cl, opts)
546 _run_parallel_tasks(task, *opts.cls)
547
548
549class ActionSubmit(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800550 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500551
552 COMMAND = 'submit'
553
554 @staticmethod
555 def _process_one(helper, cl, opts):
556 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700557 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400558
559
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500560class ActionAbandon(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800561 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500562
563 COMMAND = 'abandon'
564
565 @staticmethod
566 def _process_one(helper, cl, opts):
567 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700568 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400569
570
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500571class ActionRestore(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800572 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500573
574 COMMAND = 'restore'
575
576 @staticmethod
577 def _process_one(helper, cl, opts):
578 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700579 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400580
581
Tomasz Figa54d70992021-01-20 13:48:59 +0900582class ActionWorkInProgress(_ActionSimpleParallelCLs):
583 """Mark CLs as work in progress"""
584
585 COMMAND = 'wip'
586
587 @staticmethod
588 def _process_one(helper, cl, opts):
589 """Use |helper| to process the single |cl|."""
590 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
591
592
593class ActionReadyForReview(_ActionSimpleParallelCLs):
594 """Mark CLs as ready for review"""
595
596 COMMAND = 'ready'
597
598 @staticmethod
599 def _process_one(helper, cl, opts):
600 """Use |helper| to process the single |cl|."""
601 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
602
603
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500604class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800605 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700606
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500607 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700608
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500609 @staticmethod
610 def init_subparser(parser):
611 """Add arguments to this action's subparser."""
612 parser.add_argument('--ne', '--no-emails', dest='notify',
613 default='ALL', action='store_const', const='NONE',
614 help='Do not send e-mail notifications')
615 parser.add_argument('cl', metavar='CL',
616 help='The CL to update')
617 parser.add_argument('reviewers', nargs='+',
618 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700619
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500620 @staticmethod
621 def __call__(opts):
622 """Implement the action."""
623 # Allow for optional leading '~'.
624 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
625 add_list, remove_list, invalid_list = [], [], []
626
627 for email in opts.reviewers:
628 if not email_validator.match(email):
629 invalid_list.append(email)
630 elif email[0] == '~':
631 remove_list.append(email[1:])
632 else:
633 add_list.append(email)
634
635 if invalid_list:
636 cros_build_lib.Die(
637 'Invalid email address(es): %s' % ', '.join(invalid_list))
638
639 if add_list or remove_list:
640 helper, cl = GetGerrit(opts, opts.cl)
641 helper.SetReviewers(cl, add=add_list, remove=remove_list,
642 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700643
644
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500645class ActionAssign(_ActionSimpleParallelCLs):
646 """Set the assignee for CLs"""
647
648 COMMAND = 'assign'
649
650 @staticmethod
651 def init_subparser(parser):
652 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400653 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500654 parser.add_argument('assignee',
655 help='The new assignee')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500656
657 @staticmethod
658 def _process_one(helper, cl, opts):
659 """Use |helper| to process the single |cl|."""
660 helper.SetAssignee(cl, opts.assignee, dryrun=opts.dryrun)
Allen Li38abdaa2017-03-16 13:25:02 -0700661
662
Mike Frysinger62178ae2020-03-20 01:37:43 -0400663class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800664 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500665
666 COMMAND = 'message'
667
668 @staticmethod
669 def init_subparser(parser):
670 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400671 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500672 parser.add_argument('message',
673 help='The message to post')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500674
675 @staticmethod
676 def _process_one(helper, cl, opts):
677 """Use |helper| to process the single |cl|."""
678 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530679
680
Mike Frysinger62178ae2020-03-20 01:37:43 -0400681class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800682 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500683
684 COMMAND = 'topic'
685
686 @staticmethod
687 def init_subparser(parser):
688 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400689 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500690 parser.add_argument('topic',
691 help='The topic to set')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500692
693 @staticmethod
694 def _process_one(helper, cl, opts):
695 """Use |helper| to process the single |cl|."""
696 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800697
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800698
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500699class ActionPrivate(_ActionSimpleParallelCLs):
700 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700701
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500702 COMMAND = 'private'
703
704 @staticmethod
705 def _process_one(helper, cl, opts):
706 """Use |helper| to process the single |cl|."""
707 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700708
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800709
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500710class ActionPublic(_ActionSimpleParallelCLs):
711 """Mark CLs public"""
712
713 COMMAND = 'public'
714
715 @staticmethod
716 def _process_one(helper, cl, opts):
717 """Use |helper| to process the single |cl|."""
718 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
719
720
721class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800722 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500723
724 COMMAND = 'hashtags'
725
726 @staticmethod
727 def init_subparser(parser):
728 """Add arguments to this action's subparser."""
729 parser.add_argument('cl', metavar='CL',
730 help='The CL to update')
731 parser.add_argument('hashtags', nargs='+',
732 help='The hashtags to add/remove')
733
734 @staticmethod
735 def __call__(opts):
736 """Implement the action."""
737 add = []
738 remove = []
739 for hashtag in opts.hashtags:
740 if hashtag.startswith('~'):
741 remove.append(hashtag[1:])
742 else:
743 add.append(hashtag)
744 helper, cl = GetGerrit(opts, opts.cl)
745 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800746
747
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500748class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800749 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500750
751 COMMAND = 'deletedraft'
752
753 @staticmethod
754 def _process_one(helper, cl, opts):
755 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700756 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800757
758
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500759class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500760 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500761
762 COMMAND = 'reviewed'
763
764 @staticmethod
765 def _process_one(helper, cl, opts):
766 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500767 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500768
769
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500770class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500771 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500772
773 COMMAND = 'unreviewed'
774
775 @staticmethod
776 def _process_one(helper, cl, opts):
777 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500778 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500779
780
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500781class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500782 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500783
784 COMMAND = 'ignore'
785
786 @staticmethod
787 def _process_one(helper, cl, opts):
788 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500789 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500790
791
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500792class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500793 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500794
795 COMMAND = 'unignore'
796
797 @staticmethod
798 def _process_one(helper, cl, opts):
799 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500800 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500801
802
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400803class ActionCherryPick(UserAction):
804 """Cherry pick CLs to branches."""
805
806 COMMAND = 'cherry-pick'
807
808 @staticmethod
809 def init_subparser(parser):
810 """Add arguments to this action's subparser."""
811 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
812 parser.add_argument('--rev', '--revision', default='current',
813 help='A specific revision or patchset')
814 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
815 help='Include a message')
816 parser.add_argument('--branches', '--branch', '--br', action='split_extend',
817 default=[], required=True,
818 help='The destination branches')
819 parser.add_argument('cls', nargs='+', metavar='CL',
820 help='The CLs to cherry-pick')
821
822 @staticmethod
823 def __call__(opts):
824 """Implement the action."""
825 # Process branches in parallel, but CLs in serial in case of CL stacks.
826 def task(branch):
827 for arg in opts.cls:
828 helper, cl = GetGerrit(opts, arg)
829 ret = helper.CherryPick(cl, branch, rev=opts.rev, msg=opts.msg,
830 dryrun=opts.dryrun)
831 logging.debug('Response: %s', ret)
832 if opts.raw:
833 print(ret['_number'])
834 else:
835 uri = f'https://{helper.host}/c/{ret["_number"]}'
836 print(uri_lib.ShortenUri(uri))
837
838 _run_parallel_tasks(task, *opts.branches)
839
840
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500841class ActionAccount(_ActionSimpleParallelCLs):
842 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500843
844 COMMAND = 'account'
845
846 @staticmethod
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500847 def init_subparser(parser):
848 """Add arguments to this action's subparser."""
849 parser.add_argument('accounts', nargs='*', default=['self'],
850 help='The accounts to query')
851
852 @classmethod
853 def __call__(cls, opts):
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500854 """Implement the action."""
855 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500856
857 def print_one(header, data):
858 print(f'### {header}')
859 print(pformat.json(data, compact=opts.json).rstrip())
860
861 def task(arg):
862 detail = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/detail')
863 if not detail:
864 print(f'{arg}: account not found')
865 else:
866 print_one('detail', detail)
867 for field in ('groups', 'capabilities', 'preferences', 'sshkeys',
868 'gpgkeys'):
869 data = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/{field}')
870 print_one(field, data)
871
872 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800873
874
Mike Frysinger484e2f82020-03-20 01:41:10 -0400875class ActionHelpAll(UserAction):
876 """Show all actions help output at once."""
877
878 COMMAND = 'help-all'
879
880 @staticmethod
881 def __call__(opts):
882 """Implement the action."""
883 first = True
884 for action in _GetActions():
885 if first:
886 first = False
887 else:
888 print('\n\n')
889
890 try:
891 opts.parser.parse_args([action, '--help'])
892 except SystemExit:
893 pass
894
895
Mike Frysinger65fc8632020-02-06 18:11:12 -0500896@memoize.Memoize
897def _GetActions():
898 """Get all the possible actions we support.
899
900 Returns:
901 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
902 function that implements that command (e.g. UserActFoo).
903 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500904 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
905
906 actions = {}
907 for cls in globals().values():
908 if (not inspect.isclass(cls) or
909 not issubclass(cls, UserAction) or
910 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -0500911 continue
912
Mike Frysinger65fc8632020-02-06 18:11:12 -0500913 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500914 cmd = cls.COMMAND
915 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
916 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -0500917
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500918 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -0500919
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500920 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500921
922
Harry Cutts26076b32019-02-26 15:01:29 -0800923def _GetActionUsages():
924 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -0500925 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -0800926
Mike Frysinger65fc8632020-02-06 18:11:12 -0500927 cmds = list(actions.keys())
928 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -0800929 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500930 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -0800931
Harry Cutts26076b32019-02-26 15:01:29 -0800932 cmd_indent = len(max(cmds, key=len))
933 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500934 return '\n'.join(
935 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
936 for cmd, usage, doc in zip(cmds, usages, docs)
937 )
Harry Cutts26076b32019-02-26 15:01:29 -0800938
939
Mike Frysinger108eda22018-06-06 18:45:12 -0400940def GetParser():
941 """Returns the parser to use for this module."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500942 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -0400943There is no support for doing line-by-line code review via the command line.
944This helps you manage various bits and CL status.
945
Mike Frysingera1db2c42014-06-15 00:42:48 -0700946For general Gerrit documentation, see:
947 https://gerrit-review.googlesource.com/Documentation/
948The Searching Changes page covers the search query syntax:
949 https://gerrit-review.googlesource.com/Documentation/user-search.html
950
Mike Frysinger13f23a42013-05-13 17:32:01 -0400951Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500952 $ gerrit todo # List all the CLs that await your review.
953 $ gerrit mine # List all of your open CLs.
954 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
955 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
956 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800957 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
95828123.
959 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
960CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700961Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500962 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
963with Commit-Queue=1.
964 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
965CLs with Commit-Queue=1.
966 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400967
Harry Cutts26076b32019-02-26 15:01:29 -0800968Actions:
969"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500970 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400971
Mike Frysinger65fc8632020-02-06 18:11:12 -0500972 actions = _GetActions()
973
Alex Klein2ab29cc2018-07-19 12:01:00 -0600974 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -0400975 parser = commandline.ArgumentParser(
976 description=description, default_log_level='notice')
Mike Frysinger08737512014-02-07 22:58:26 -0500977 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600978 default=site_params.EXTERNAL_GOB_INSTANCE,
979 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500980 help='Query internal Chromium Gerrit instance')
981 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600982 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500983 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600984 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700985 parser.add_argument('--raw', default=False, action='store_true',
986 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400987 parser.add_argument('--json', default=False, action='store_true',
988 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700989 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
990 dest='dryrun',
991 help='Show what would be done, but do not make changes')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500992
993 # Subparsers are required by default under Python 2. Python 3 changed to
994 # not required, but didn't include a required option until 3.7. Setting
995 # the required member works in all versions (and setting dest name).
996 subparsers = parser.add_subparsers(dest='action')
997 subparsers.required = True
998 for cmd, cls in actions.items():
999 # Format the full docstring by removing the file level indentation.
1000 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
1001 subparser = subparsers.add_parser(cmd, description=description)
1002 subparser.add_argument('-n', '--dry-run', dest='dryrun',
1003 default=False, action='store_true',
1004 help='Show what would be done only')
1005 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001006
1007 return parser
1008
1009
1010def main(argv):
1011 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -05001012 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001013
Mike Frysinger484e2f82020-03-20 01:41:10 -04001014 # In case the action wants to throw a parser error.
1015 opts.parser = parser
1016
Mike Frysinger88f27292014-06-17 09:40:45 -07001017 # A cache of gerrit helpers we'll load on demand.
1018 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001019
Mike Frysinger88f27292014-06-17 09:40:45 -07001020 opts.Freeze()
1021
Mike Frysinger27e21b72018-07-12 14:20:21 -04001022 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001023 global COLOR
1024 COLOR = terminal.Color(enabled=opts.color)
1025
Mike Frysinger13f23a42013-05-13 17:32:01 -04001026 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001027 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001028 obj = actions[opts.action]()
Mike Frysinger65fc8632020-02-06 18:11:12 -05001029 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001030 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001031 except (cros_build_lib.RunCommandError, gerrit.GerritException,
1032 gob_util.GOBError) as e:
1033 cros_build_lib.Die(e)