blob: 7cf8928fcbf302a9dc118c914a8cfb669f58f85b [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."""
476 parser.add_argument('--ne', '--no-emails', dest='notify',
477 default='ALL', action='store_const', const='NONE',
478 help='Do not send e-mail notifications')
479 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
480 help='Optional message to include')
481 parser.add_argument('cls', nargs='+', metavar='CL',
482 help='The CL(s) to update')
483 parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
484 help='The label value; one of [%(choices)s]')
485
486 @classmethod
487 def __call__(cls, opts):
488 """Implement the action."""
489 # Convert user friendly command line option into a gerrit parameter.
490 def task(arg):
491 helper, cl = GetGerrit(opts, arg)
492 helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
493 dryrun=opts.dryrun, notify=opts.notify)
494 _run_parallel_tasks(task, *opts.cls)
495
496
497class ActionLabelAutoSubmit(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500498 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500499
500 COMMAND = 'label-as'
501 LABEL = 'Auto-Submit'
502 VALUES = ('0', '1')
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600503
504
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500505class ActionLabelCodeReview(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500506 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500507
508 COMMAND = 'label-cr'
509 LABEL = 'Code-Review'
510 VALUES = ('-2', '-1', '0', '1', '2')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400511
512
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500513class ActionLabelVerified(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500514 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500515
516 COMMAND = 'label-v'
517 LABEL = 'Verified'
518 VALUES = ('-1', '0', '1')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400519
520
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500521class ActionLabelCommitQueue(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500522 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500523
524 COMMAND = 'label-cq'
525 LABEL = 'Commit-Queue'
526 VALUES = ('0', '1', '2')
Mike Frysinger15b23e42014-12-05 17:00:05 -0500527
528
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500529class _ActionSimpleParallelCLs(UserAction):
530 """Base helper for actions that only accept CLs."""
531
532 @staticmethod
533 def init_subparser(parser):
534 """Add arguments to this action's subparser."""
535 parser.add_argument('cls', nargs='+', metavar='CL',
536 help='The CL(s) to update')
537
538 def __call__(self, opts):
539 """Implement the action."""
540 def task(arg):
541 helper, cl = GetGerrit(opts, arg)
542 self._process_one(helper, cl, opts)
543 _run_parallel_tasks(task, *opts.cls)
544
545
546class ActionSubmit(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800547 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500548
549 COMMAND = 'submit'
550
551 @staticmethod
552 def _process_one(helper, cl, opts):
553 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700554 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400555
556
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500557class ActionAbandon(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800558 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500559
560 COMMAND = 'abandon'
561
562 @staticmethod
563 def _process_one(helper, cl, opts):
564 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700565 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400566
567
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500568class ActionRestore(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800569 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500570
571 COMMAND = 'restore'
572
573 @staticmethod
574 def _process_one(helper, cl, opts):
575 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700576 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400577
578
Tomasz Figa54d70992021-01-20 13:48:59 +0900579class ActionWorkInProgress(_ActionSimpleParallelCLs):
580 """Mark CLs as work in progress"""
581
582 COMMAND = 'wip'
583
584 @staticmethod
585 def _process_one(helper, cl, opts):
586 """Use |helper| to process the single |cl|."""
587 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
588
589
590class ActionReadyForReview(_ActionSimpleParallelCLs):
591 """Mark CLs as ready for review"""
592
593 COMMAND = 'ready'
594
595 @staticmethod
596 def _process_one(helper, cl, opts):
597 """Use |helper| to process the single |cl|."""
598 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
599
600
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500601class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800602 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700603
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500604 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700605
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500606 @staticmethod
607 def init_subparser(parser):
608 """Add arguments to this action's subparser."""
609 parser.add_argument('--ne', '--no-emails', dest='notify',
610 default='ALL', action='store_const', const='NONE',
611 help='Do not send e-mail notifications')
612 parser.add_argument('cl', metavar='CL',
613 help='The CL to update')
614 parser.add_argument('reviewers', nargs='+',
615 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700616
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500617 @staticmethod
618 def __call__(opts):
619 """Implement the action."""
620 # Allow for optional leading '~'.
621 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
622 add_list, remove_list, invalid_list = [], [], []
623
624 for email in opts.reviewers:
625 if not email_validator.match(email):
626 invalid_list.append(email)
627 elif email[0] == '~':
628 remove_list.append(email[1:])
629 else:
630 add_list.append(email)
631
632 if invalid_list:
633 cros_build_lib.Die(
634 'Invalid email address(es): %s' % ', '.join(invalid_list))
635
636 if add_list or remove_list:
637 helper, cl = GetGerrit(opts, opts.cl)
638 helper.SetReviewers(cl, add=add_list, remove=remove_list,
639 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700640
641
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500642class ActionAssign(_ActionSimpleParallelCLs):
643 """Set the assignee for CLs"""
644
645 COMMAND = 'assign'
646
647 @staticmethod
648 def init_subparser(parser):
649 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400650 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500651 parser.add_argument('assignee',
652 help='The new assignee')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500653
654 @staticmethod
655 def _process_one(helper, cl, opts):
656 """Use |helper| to process the single |cl|."""
657 helper.SetAssignee(cl, opts.assignee, dryrun=opts.dryrun)
Allen Li38abdaa2017-03-16 13:25:02 -0700658
659
Mike Frysinger62178ae2020-03-20 01:37:43 -0400660class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800661 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500662
663 COMMAND = 'message'
664
665 @staticmethod
666 def init_subparser(parser):
667 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400668 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500669 parser.add_argument('message',
670 help='The message to post')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500671
672 @staticmethod
673 def _process_one(helper, cl, opts):
674 """Use |helper| to process the single |cl|."""
675 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530676
677
Mike Frysinger62178ae2020-03-20 01:37:43 -0400678class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800679 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500680
681 COMMAND = 'topic'
682
683 @staticmethod
684 def init_subparser(parser):
685 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400686 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500687 parser.add_argument('topic',
688 help='The topic to set')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500689
690 @staticmethod
691 def _process_one(helper, cl, opts):
692 """Use |helper| to process the single |cl|."""
693 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800694
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800695
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500696class ActionPrivate(_ActionSimpleParallelCLs):
697 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700698
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500699 COMMAND = 'private'
700
701 @staticmethod
702 def _process_one(helper, cl, opts):
703 """Use |helper| to process the single |cl|."""
704 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700705
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800706
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500707class ActionPublic(_ActionSimpleParallelCLs):
708 """Mark CLs public"""
709
710 COMMAND = 'public'
711
712 @staticmethod
713 def _process_one(helper, cl, opts):
714 """Use |helper| to process the single |cl|."""
715 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
716
717
718class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800719 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500720
721 COMMAND = 'hashtags'
722
723 @staticmethod
724 def init_subparser(parser):
725 """Add arguments to this action's subparser."""
726 parser.add_argument('cl', metavar='CL',
727 help='The CL to update')
728 parser.add_argument('hashtags', nargs='+',
729 help='The hashtags to add/remove')
730
731 @staticmethod
732 def __call__(opts):
733 """Implement the action."""
734 add = []
735 remove = []
736 for hashtag in opts.hashtags:
737 if hashtag.startswith('~'):
738 remove.append(hashtag[1:])
739 else:
740 add.append(hashtag)
741 helper, cl = GetGerrit(opts, opts.cl)
742 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800743
744
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500745class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800746 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500747
748 COMMAND = 'deletedraft'
749
750 @staticmethod
751 def _process_one(helper, cl, opts):
752 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700753 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800754
755
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500756class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500757 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500758
759 COMMAND = 'reviewed'
760
761 @staticmethod
762 def _process_one(helper, cl, opts):
763 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500764 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500765
766
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500767class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500768 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500769
770 COMMAND = 'unreviewed'
771
772 @staticmethod
773 def _process_one(helper, cl, opts):
774 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500775 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500776
777
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500778class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500779 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500780
781 COMMAND = 'ignore'
782
783 @staticmethod
784 def _process_one(helper, cl, opts):
785 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500786 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500787
788
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500789class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500790 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500791
792 COMMAND = 'unignore'
793
794 @staticmethod
795 def _process_one(helper, cl, opts):
796 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500797 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500798
799
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400800class ActionCherryPick(UserAction):
801 """Cherry pick CLs to branches."""
802
803 COMMAND = 'cherry-pick'
804
805 @staticmethod
806 def init_subparser(parser):
807 """Add arguments to this action's subparser."""
808 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
809 parser.add_argument('--rev', '--revision', default='current',
810 help='A specific revision or patchset')
811 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
812 help='Include a message')
813 parser.add_argument('--branches', '--branch', '--br', action='split_extend',
814 default=[], required=True,
815 help='The destination branches')
816 parser.add_argument('cls', nargs='+', metavar='CL',
817 help='The CLs to cherry-pick')
818
819 @staticmethod
820 def __call__(opts):
821 """Implement the action."""
822 # Process branches in parallel, but CLs in serial in case of CL stacks.
823 def task(branch):
824 for arg in opts.cls:
825 helper, cl = GetGerrit(opts, arg)
826 ret = helper.CherryPick(cl, branch, rev=opts.rev, msg=opts.msg,
827 dryrun=opts.dryrun)
828 logging.debug('Response: %s', ret)
829 if opts.raw:
830 print(ret['_number'])
831 else:
832 uri = f'https://{helper.host}/c/{ret["_number"]}'
833 print(uri_lib.ShortenUri(uri))
834
835 _run_parallel_tasks(task, *opts.branches)
836
837
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500838class ActionAccount(_ActionSimpleParallelCLs):
839 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500840
841 COMMAND = 'account'
842
843 @staticmethod
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500844 def init_subparser(parser):
845 """Add arguments to this action's subparser."""
846 parser.add_argument('accounts', nargs='*', default=['self'],
847 help='The accounts to query')
848
849 @classmethod
850 def __call__(cls, opts):
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500851 """Implement the action."""
852 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500853
854 def print_one(header, data):
855 print(f'### {header}')
856 print(pformat.json(data, compact=opts.json).rstrip())
857
858 def task(arg):
859 detail = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/detail')
860 if not detail:
861 print(f'{arg}: account not found')
862 else:
863 print_one('detail', detail)
864 for field in ('groups', 'capabilities', 'preferences', 'sshkeys',
865 'gpgkeys'):
866 data = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/{field}')
867 print_one(field, data)
868
869 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800870
871
Mike Frysinger484e2f82020-03-20 01:41:10 -0400872class ActionHelpAll(UserAction):
873 """Show all actions help output at once."""
874
875 COMMAND = 'help-all'
876
877 @staticmethod
878 def __call__(opts):
879 """Implement the action."""
880 first = True
881 for action in _GetActions():
882 if first:
883 first = False
884 else:
885 print('\n\n')
886
887 try:
888 opts.parser.parse_args([action, '--help'])
889 except SystemExit:
890 pass
891
892
Mike Frysinger65fc8632020-02-06 18:11:12 -0500893@memoize.Memoize
894def _GetActions():
895 """Get all the possible actions we support.
896
897 Returns:
898 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
899 function that implements that command (e.g. UserActFoo).
900 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500901 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
902
903 actions = {}
904 for cls in globals().values():
905 if (not inspect.isclass(cls) or
906 not issubclass(cls, UserAction) or
907 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -0500908 continue
909
Mike Frysinger65fc8632020-02-06 18:11:12 -0500910 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500911 cmd = cls.COMMAND
912 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
913 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -0500914
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500915 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -0500916
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500917 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500918
919
Harry Cutts26076b32019-02-26 15:01:29 -0800920def _GetActionUsages():
921 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -0500922 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -0800923
Mike Frysinger65fc8632020-02-06 18:11:12 -0500924 cmds = list(actions.keys())
925 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -0800926 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500927 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -0800928
Harry Cutts26076b32019-02-26 15:01:29 -0800929 cmd_indent = len(max(cmds, key=len))
930 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500931 return '\n'.join(
932 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
933 for cmd, usage, doc in zip(cmds, usages, docs)
934 )
Harry Cutts26076b32019-02-26 15:01:29 -0800935
936
Mike Frysinger108eda22018-06-06 18:45:12 -0400937def GetParser():
938 """Returns the parser to use for this module."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500939 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -0400940There is no support for doing line-by-line code review via the command line.
941This helps you manage various bits and CL status.
942
Mike Frysingera1db2c42014-06-15 00:42:48 -0700943For general Gerrit documentation, see:
944 https://gerrit-review.googlesource.com/Documentation/
945The Searching Changes page covers the search query syntax:
946 https://gerrit-review.googlesource.com/Documentation/user-search.html
947
Mike Frysinger13f23a42013-05-13 17:32:01 -0400948Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500949 $ gerrit todo # List all the CLs that await your review.
950 $ gerrit mine # List all of your open CLs.
951 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
952 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
953 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800954 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
95528123.
956 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
957CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700958Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500959 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
960with Commit-Queue=1.
961 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
962CLs with Commit-Queue=1.
963 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400964
Harry Cutts26076b32019-02-26 15:01:29 -0800965Actions:
966"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500967 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400968
Mike Frysinger65fc8632020-02-06 18:11:12 -0500969 actions = _GetActions()
970
Alex Klein2ab29cc2018-07-19 12:01:00 -0600971 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -0400972 parser = commandline.ArgumentParser(
973 description=description, default_log_level='notice')
Mike Frysinger08737512014-02-07 22:58:26 -0500974 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600975 default=site_params.EXTERNAL_GOB_INSTANCE,
976 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500977 help='Query internal Chromium Gerrit instance')
978 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600979 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500980 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600981 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700982 parser.add_argument('--raw', default=False, action='store_true',
983 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400984 parser.add_argument('--json', default=False, action='store_true',
985 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700986 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
987 dest='dryrun',
988 help='Show what would be done, but do not make changes')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500989
990 # Subparsers are required by default under Python 2. Python 3 changed to
991 # not required, but didn't include a required option until 3.7. Setting
992 # the required member works in all versions (and setting dest name).
993 subparsers = parser.add_subparsers(dest='action')
994 subparsers.required = True
995 for cmd, cls in actions.items():
996 # Format the full docstring by removing the file level indentation.
997 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
998 subparser = subparsers.add_parser(cmd, description=description)
999 subparser.add_argument('-n', '--dry-run', dest='dryrun',
1000 default=False, action='store_true',
1001 help='Show what would be done only')
1002 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001003
1004 return parser
1005
1006
1007def main(argv):
1008 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -05001009 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001010
Mike Frysinger484e2f82020-03-20 01:41:10 -04001011 # In case the action wants to throw a parser error.
1012 opts.parser = parser
1013
Mike Frysinger88f27292014-06-17 09:40:45 -07001014 # A cache of gerrit helpers we'll load on demand.
1015 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001016
Mike Frysinger88f27292014-06-17 09:40:45 -07001017 opts.Freeze()
1018
Mike Frysinger27e21b72018-07-12 14:20:21 -04001019 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001020 global COLOR
1021 COLOR = terminal.Color(enabled=opts.color)
1022
Mike Frysinger13f23a42013-05-13 17:32:01 -04001023 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001024 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001025 obj = actions[opts.action]()
Mike Frysinger65fc8632020-02-06 18:11:12 -05001026 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001027 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001028 except (cros_build_lib.RunCommandError, gerrit.GerritException,
1029 gob_util.GOBError) as e:
1030 cros_build_lib.Die(e)