blob: a32f0c28a05018817aebea91e9dfc308e4fd2759 [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 Frysinger7cbd88c2021-02-12 03:52:25 -0500435 """Yields the Gerrit dependencies of a patch"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500436 for change in cls._ProcessDeps(
437 opts, querier, cl, cl.GerritDependencies(), False):
Mike Frysinger5726da92017-09-20 22:14:25 -0400438 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700439
Paul Hobbs89765232015-06-24 14:07:49 -0700440
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500441class ActionInspect(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800442 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500443
444 COMMAND = 'inspect'
445
446 @staticmethod
447 def init_subparser(parser):
448 """Add arguments to this action's subparser."""
449 _ActionSearchQuery.init_subparser(parser)
450 parser.add_argument('cls', nargs='+', metavar='CL',
451 help='The CL(s) to update')
452
453 @staticmethod
454 def __call__(opts):
455 """Implement the action."""
456 cls = []
457 for arg in opts.cls:
458 helper, cl = GetGerrit(opts, arg)
459 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
460 if change:
461 cls.extend(change)
462 else:
463 logging.warning('no results found for CL %s', arg)
464 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400465
466
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500467class _ActionLabeler(UserAction):
468 """Base helper for setting labels."""
469
470 LABEL = None
471 VALUES = None
472
473 @classmethod
474 def init_subparser(cls, parser):
475 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500476 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
477 help='Optional message to include')
478 parser.add_argument('cls', nargs='+', metavar='CL',
479 help='The CL(s) to update')
480 parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
481 help='The label value; one of [%(choices)s]')
482
483 @classmethod
484 def __call__(cls, opts):
485 """Implement the action."""
486 # Convert user friendly command line option into a gerrit parameter.
487 def task(arg):
488 helper, cl = GetGerrit(opts, arg)
489 helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
490 dryrun=opts.dryrun, notify=opts.notify)
491 _run_parallel_tasks(task, *opts.cls)
492
493
494class ActionLabelAutoSubmit(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500495 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500496
497 COMMAND = 'label-as'
498 LABEL = 'Auto-Submit'
499 VALUES = ('0', '1')
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600500
501
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500502class ActionLabelCodeReview(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500503 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500504
505 COMMAND = 'label-cr'
506 LABEL = 'Code-Review'
507 VALUES = ('-2', '-1', '0', '1', '2')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400508
509
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500510class ActionLabelVerified(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500511 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500512
513 COMMAND = 'label-v'
514 LABEL = 'Verified'
515 VALUES = ('-1', '0', '1')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400516
517
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500518class ActionLabelCommitQueue(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500519 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500520
521 COMMAND = 'label-cq'
522 LABEL = 'Commit-Queue'
523 VALUES = ('0', '1', '2')
Mike Frysinger15b23e42014-12-05 17:00:05 -0500524
525
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500526class _ActionSimpleParallelCLs(UserAction):
527 """Base helper for actions that only accept CLs."""
528
529 @staticmethod
530 def init_subparser(parser):
531 """Add arguments to this action's subparser."""
532 parser.add_argument('cls', nargs='+', metavar='CL',
533 help='The CL(s) to update')
534
535 def __call__(self, opts):
536 """Implement the action."""
537 def task(arg):
538 helper, cl = GetGerrit(opts, arg)
539 self._process_one(helper, cl, opts)
540 _run_parallel_tasks(task, *opts.cls)
541
542
543class ActionSubmit(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800544 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500545
546 COMMAND = 'submit'
547
548 @staticmethod
549 def _process_one(helper, cl, opts):
550 """Use |helper| to process the single |cl|."""
Mike Frysinger8674a112021-02-09 14:44:17 -0500551 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400552
553
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500554class ActionAbandon(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800555 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500556
557 COMMAND = 'abandon'
558
559 @staticmethod
560 def _process_one(helper, cl, opts):
561 """Use |helper| to process the single |cl|."""
Mike Frysinger8674a112021-02-09 14:44:17 -0500562 helper.AbandonChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400563
564
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500565class ActionRestore(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800566 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500567
568 COMMAND = 'restore'
569
570 @staticmethod
571 def _process_one(helper, cl, opts):
572 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700573 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400574
575
Tomasz Figa54d70992021-01-20 13:48:59 +0900576class ActionWorkInProgress(_ActionSimpleParallelCLs):
577 """Mark CLs as work in progress"""
578
579 COMMAND = 'wip'
580
581 @staticmethod
582 def _process_one(helper, cl, opts):
583 """Use |helper| to process the single |cl|."""
584 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
585
586
587class ActionReadyForReview(_ActionSimpleParallelCLs):
588 """Mark CLs as ready for review"""
589
590 COMMAND = 'ready'
591
592 @staticmethod
593 def _process_one(helper, cl, opts):
594 """Use |helper| to process the single |cl|."""
595 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
596
597
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500598class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800599 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700600
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500601 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700602
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500603 @staticmethod
604 def init_subparser(parser):
605 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500606 parser.add_argument('cl', metavar='CL',
607 help='The CL to update')
608 parser.add_argument('reviewers', nargs='+',
609 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700610
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500611 @staticmethod
612 def __call__(opts):
613 """Implement the action."""
614 # Allow for optional leading '~'.
615 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
616 add_list, remove_list, invalid_list = [], [], []
617
618 for email in opts.reviewers:
619 if not email_validator.match(email):
620 invalid_list.append(email)
621 elif email[0] == '~':
622 remove_list.append(email[1:])
623 else:
624 add_list.append(email)
625
626 if invalid_list:
627 cros_build_lib.Die(
628 'Invalid email address(es): %s' % ', '.join(invalid_list))
629
630 if add_list or remove_list:
631 helper, cl = GetGerrit(opts, opts.cl)
632 helper.SetReviewers(cl, add=add_list, remove=remove_list,
633 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700634
635
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500636class ActionAssign(_ActionSimpleParallelCLs):
637 """Set the assignee for CLs"""
638
639 COMMAND = 'assign'
640
641 @staticmethod
642 def init_subparser(parser):
643 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400644 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500645 parser.add_argument('assignee',
646 help='The new assignee')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500647
648 @staticmethod
649 def _process_one(helper, cl, opts):
650 """Use |helper| to process the single |cl|."""
651 helper.SetAssignee(cl, opts.assignee, dryrun=opts.dryrun)
Allen Li38abdaa2017-03-16 13:25:02 -0700652
653
Mike Frysinger62178ae2020-03-20 01:37:43 -0400654class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800655 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500656
657 COMMAND = 'message'
658
659 @staticmethod
660 def init_subparser(parser):
661 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400662 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500663 parser.add_argument('message',
664 help='The message to post')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500665
666 @staticmethod
667 def _process_one(helper, cl, opts):
668 """Use |helper| to process the single |cl|."""
669 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530670
671
Mike Frysinger62178ae2020-03-20 01:37:43 -0400672class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800673 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500674
675 COMMAND = 'topic'
676
677 @staticmethod
678 def init_subparser(parser):
679 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400680 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500681 parser.add_argument('topic',
682 help='The topic to set')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500683
684 @staticmethod
685 def _process_one(helper, cl, opts):
686 """Use |helper| to process the single |cl|."""
687 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800688
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800689
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500690class ActionPrivate(_ActionSimpleParallelCLs):
691 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700692
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500693 COMMAND = 'private'
694
695 @staticmethod
696 def _process_one(helper, cl, opts):
697 """Use |helper| to process the single |cl|."""
698 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700699
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800700
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500701class ActionPublic(_ActionSimpleParallelCLs):
702 """Mark CLs public"""
703
704 COMMAND = 'public'
705
706 @staticmethod
707 def _process_one(helper, cl, opts):
708 """Use |helper| to process the single |cl|."""
709 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
710
711
712class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800713 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500714
715 COMMAND = 'hashtags'
716
717 @staticmethod
718 def init_subparser(parser):
719 """Add arguments to this action's subparser."""
720 parser.add_argument('cl', metavar='CL',
721 help='The CL to update')
722 parser.add_argument('hashtags', nargs='+',
723 help='The hashtags to add/remove')
724
725 @staticmethod
726 def __call__(opts):
727 """Implement the action."""
728 add = []
729 remove = []
730 for hashtag in opts.hashtags:
731 if hashtag.startswith('~'):
732 remove.append(hashtag[1:])
733 else:
734 add.append(hashtag)
735 helper, cl = GetGerrit(opts, opts.cl)
736 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800737
738
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500739class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800740 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500741
742 COMMAND = 'deletedraft'
743
744 @staticmethod
745 def _process_one(helper, cl, opts):
746 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700747 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800748
749
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500750class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500751 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500752
753 COMMAND = 'reviewed'
754
755 @staticmethod
756 def _process_one(helper, cl, opts):
757 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500758 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500759
760
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500761class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500762 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500763
764 COMMAND = 'unreviewed'
765
766 @staticmethod
767 def _process_one(helper, cl, opts):
768 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500769 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500770
771
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500772class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500773 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500774
775 COMMAND = 'ignore'
776
777 @staticmethod
778 def _process_one(helper, cl, opts):
779 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500780 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500781
782
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500783class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500784 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500785
786 COMMAND = 'unignore'
787
788 @staticmethod
789 def _process_one(helper, cl, opts):
790 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500791 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500792
793
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400794class ActionCherryPick(UserAction):
795 """Cherry pick CLs to branches."""
796
797 COMMAND = 'cherry-pick'
798
799 @staticmethod
800 def init_subparser(parser):
801 """Add arguments to this action's subparser."""
802 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
803 parser.add_argument('--rev', '--revision', default='current',
804 help='A specific revision or patchset')
805 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
806 help='Include a message')
807 parser.add_argument('--branches', '--branch', '--br', action='split_extend',
808 default=[], required=True,
809 help='The destination branches')
810 parser.add_argument('cls', nargs='+', metavar='CL',
811 help='The CLs to cherry-pick')
812
813 @staticmethod
814 def __call__(opts):
815 """Implement the action."""
816 # Process branches in parallel, but CLs in serial in case of CL stacks.
817 def task(branch):
818 for arg in opts.cls:
819 helper, cl = GetGerrit(opts, arg)
820 ret = helper.CherryPick(cl, branch, rev=opts.rev, msg=opts.msg,
Mike Frysinger8674a112021-02-09 14:44:17 -0500821 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400822 logging.debug('Response: %s', ret)
823 if opts.raw:
824 print(ret['_number'])
825 else:
826 uri = f'https://{helper.host}/c/{ret["_number"]}'
827 print(uri_lib.ShortenUri(uri))
828
829 _run_parallel_tasks(task, *opts.branches)
830
831
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500832class ActionAccount(_ActionSimpleParallelCLs):
833 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500834
835 COMMAND = 'account'
836
837 @staticmethod
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500838 def init_subparser(parser):
839 """Add arguments to this action's subparser."""
840 parser.add_argument('accounts', nargs='*', default=['self'],
841 help='The accounts to query')
842
843 @classmethod
844 def __call__(cls, opts):
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500845 """Implement the action."""
846 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500847
848 def print_one(header, data):
849 print(f'### {header}')
850 print(pformat.json(data, compact=opts.json).rstrip())
851
852 def task(arg):
853 detail = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/detail')
854 if not detail:
855 print(f'{arg}: account not found')
856 else:
857 print_one('detail', detail)
858 for field in ('groups', 'capabilities', 'preferences', 'sshkeys',
859 'gpgkeys'):
860 data = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/{field}')
861 print_one(field, data)
862
863 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800864
865
Mike Frysinger484e2f82020-03-20 01:41:10 -0400866class ActionHelpAll(UserAction):
867 """Show all actions help output at once."""
868
869 COMMAND = 'help-all'
870
871 @staticmethod
872 def __call__(opts):
873 """Implement the action."""
874 first = True
875 for action in _GetActions():
876 if first:
877 first = False
878 else:
879 print('\n\n')
880
881 try:
882 opts.parser.parse_args([action, '--help'])
883 except SystemExit:
884 pass
885
886
Mike Frysinger65fc8632020-02-06 18:11:12 -0500887@memoize.Memoize
888def _GetActions():
889 """Get all the possible actions we support.
890
891 Returns:
892 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
893 function that implements that command (e.g. UserActFoo).
894 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500895 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
896
897 actions = {}
898 for cls in globals().values():
899 if (not inspect.isclass(cls) or
900 not issubclass(cls, UserAction) or
901 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -0500902 continue
903
Mike Frysinger65fc8632020-02-06 18:11:12 -0500904 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500905 cmd = cls.COMMAND
906 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
907 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -0500908
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500909 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -0500910
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500911 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500912
913
Harry Cutts26076b32019-02-26 15:01:29 -0800914def _GetActionUsages():
915 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -0500916 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -0800917
Mike Frysinger65fc8632020-02-06 18:11:12 -0500918 cmds = list(actions.keys())
919 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -0800920 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500921 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -0800922
Harry Cutts26076b32019-02-26 15:01:29 -0800923 cmd_indent = len(max(cmds, key=len))
924 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500925 return '\n'.join(
926 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
927 for cmd, usage, doc in zip(cmds, usages, docs)
928 )
Harry Cutts26076b32019-02-26 15:01:29 -0800929
930
Mike Frysinger108eda22018-06-06 18:45:12 -0400931def GetParser():
932 """Returns the parser to use for this module."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500933 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -0400934There is no support for doing line-by-line code review via the command line.
935This helps you manage various bits and CL status.
936
Mike Frysingera1db2c42014-06-15 00:42:48 -0700937For general Gerrit documentation, see:
938 https://gerrit-review.googlesource.com/Documentation/
939The Searching Changes page covers the search query syntax:
940 https://gerrit-review.googlesource.com/Documentation/user-search.html
941
Mike Frysinger13f23a42013-05-13 17:32:01 -0400942Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500943 $ gerrit todo # List all the CLs that await your review.
944 $ gerrit mine # List all of your open CLs.
945 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
946 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
947 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800948 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
94928123.
950 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
951CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700952Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500953 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
954with Commit-Queue=1.
955 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
956CLs with Commit-Queue=1.
957 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400958
Harry Cutts26076b32019-02-26 15:01:29 -0800959Actions:
960"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500961 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400962
Mike Frysinger65fc8632020-02-06 18:11:12 -0500963 actions = _GetActions()
964
Alex Klein2ab29cc2018-07-19 12:01:00 -0600965 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -0400966 parser = commandline.ArgumentParser(
967 description=description, default_log_level='notice')
Mike Frysinger8674a112021-02-09 14:44:17 -0500968
969 group = parser.add_argument_group('Server options')
970 group.add_argument('-i', '--internal', dest='gob', action='store_const',
971 default=site_params.EXTERNAL_GOB_INSTANCE,
972 const=site_params.INTERNAL_GOB_INSTANCE,
973 help='Query internal Chrome Gerrit instance')
974 group.add_argument('-g', '--gob',
975 default=site_params.EXTERNAL_GOB_INSTANCE,
976 help='Gerrit (on borg) instance to query (default: %s)' %
977 (site_params.EXTERNAL_GOB_INSTANCE))
978
979 def _AddCommonOptions(p):
980 """Add options that should work before & after the subcommand.
981
982 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
983 """
984 parser.add_common_argument_to_group(
985 p, '--ne', '--no-emails', dest='notify',
986 default='ALL', action='store_const', const='NONE',
987 help='Do not send e-mail notifications')
988 parser.add_common_argument_to_group(
989 p, '-n', '--dry-run', dest='dryrun',
990 default=False, action='store_true',
991 help='Show what would be done, but do not make changes')
992
993 group = parser.add_argument_group('CL options')
994 _AddCommonOptions(group)
995
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700996 parser.add_argument('--raw', default=False, action='store_true',
997 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400998 parser.add_argument('--json', default=False, action='store_true',
999 help='Return results in JSON (suitable for scripting)')
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001000
1001 # Subparsers are required by default under Python 2. Python 3 changed to
1002 # not required, but didn't include a required option until 3.7. Setting
1003 # the required member works in all versions (and setting dest name).
1004 subparsers = parser.add_subparsers(dest='action')
1005 subparsers.required = True
1006 for cmd, cls in actions.items():
1007 # Format the full docstring by removing the file level indentation.
1008 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
1009 subparser = subparsers.add_parser(cmd, description=description)
Mike Frysinger8674a112021-02-09 14:44:17 -05001010 _AddCommonOptions(subparser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001011 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001012
1013 return parser
1014
1015
1016def main(argv):
1017 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -05001018 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001019
Mike Frysinger484e2f82020-03-20 01:41:10 -04001020 # In case the action wants to throw a parser error.
1021 opts.parser = parser
1022
Mike Frysinger88f27292014-06-17 09:40:45 -07001023 # A cache of gerrit helpers we'll load on demand.
1024 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001025
Mike Frysinger88f27292014-06-17 09:40:45 -07001026 opts.Freeze()
1027
Mike Frysinger27e21b72018-07-12 14:20:21 -04001028 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001029 global COLOR
1030 COLOR = terminal.Color(enabled=opts.color)
1031
Mike Frysinger13f23a42013-05-13 17:32:01 -04001032 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001033 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001034 obj = actions[opts.action]()
Mike Frysinger65fc8632020-02-06 18:11:12 -05001035 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001036 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001037 except (cros_build_lib.RunCommandError, gerrit.GerritException,
1038 gob_util.GOBError) as e:
1039 cros_build_lib.Die(e)