blob: b345f534e09535859372a4e7a60ed64ba632f496 [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 Frysinger13f23a42013-05-13 17:32:01 -040030from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040031from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060032from chromite.utils import memoize
Mike Frysinger13f23a42013-05-13 17:32:01 -040033
34
Mike Frysinger1c76d4c2020-02-08 23:35:29 -050035assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
36
37
Mike Frysingerc7796cf2020-02-06 23:55:15 -050038class UserAction(object):
39 """Base class for all custom user actions."""
40
41 # The name of the command the user types in.
42 COMMAND = None
43
44 @staticmethod
45 def init_subparser(parser):
46 """Add arguments to this action's subparser."""
47
48 @staticmethod
49 def __call__(opts):
50 """Implement the action."""
Mike Frysinger62178ae2020-03-20 01:37:43 -040051 raise RuntimeError('Internal error: action missing __call__ implementation')
Mike Frysinger108eda22018-06-06 18:45:12 -040052
53
Mike Frysinger254f33f2019-12-11 13:54:29 -050054# How many connections we'll use in parallel. We don't want this to be too high
55# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
56# seems to be good enough for users.
57CONNECTION_LIMIT = 10
58
59
Mike Frysinger031ad0b2013-05-14 18:15:34 -040060COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040061
62# Map the internal names to the ones we normally show on the web ui.
63GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080064 'COMR': ['CQ', 'Commit Queue ',],
65 'CRVW': ['CR', 'Code Review ',],
66 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080067 'VRIF': ['V ', 'Verified ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060068 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040069}
70
71# Order is important -- matches the web ui. This also controls the short
72# entries that we summarize in non-verbose mode.
73GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
74
Mike Frysinger4aea5dc2019-07-17 13:39:56 -040075# Shorter strings for CL status messages.
76GERRIT_SUMMARY_MAP = {
77 'ABANDONED': 'ABD',
78 'MERGED': 'MRG',
79 'NEW': 'NEW',
80 'WIP': 'WIP',
81}
82
Mike Frysinger13f23a42013-05-13 17:32:01 -040083
84def red(s):
85 return COLOR.Color(terminal.Color.RED, s)
86
87
88def green(s):
89 return COLOR.Color(terminal.Color.GREEN, s)
90
91
92def blue(s):
93 return COLOR.Color(terminal.Color.BLUE, s)
94
95
Mike Frysinger254f33f2019-12-11 13:54:29 -050096def _run_parallel_tasks(task, *args):
97 """Small wrapper around BackgroundTaskRunner to enforce job count."""
98 with parallel.BackgroundTaskRunner(task, processes=CONNECTION_LIMIT) as q:
99 for arg in args:
100 q.put([arg])
101
102
Mike Frysinger13f23a42013-05-13 17:32:01 -0400103def limits(cls):
104 """Given a dict of fields, calculate the longest string lengths
105
106 This allows you to easily format the output of many results so that the
107 various cols all line up correctly.
108 """
109 lims = {}
110 for cl in cls:
111 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -0400112 # Use %s rather than str() to avoid codec issues.
113 # We also do this so we can format integers.
114 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400115 return lims
116
117
Mike Frysinger88f27292014-06-17 09:40:45 -0700118# TODO: This func really needs to be merged into the core gerrit logic.
119def GetGerrit(opts, cl=None):
120 """Auto pick the right gerrit instance based on the |cl|
121
122 Args:
123 opts: The general options object.
124 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
125
126 Returns:
127 A tuple of a gerrit object and a sanitized CL #.
128 """
129 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700130 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600131 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600132 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600133 if cl.startswith('*'):
134 cl = cl[1:]
135 else:
136 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700137 elif ':' in cl:
138 gob, cl = cl.split(':', 1)
139
140 if not gob in opts.gerrit:
141 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
142
143 return (opts.gerrit[gob], cl)
144
145
Mike Frysinger13f23a42013-05-13 17:32:01 -0400146def GetApprovalSummary(_opts, cls):
147 """Return a dict of the most important approvals"""
148 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700149 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
150 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
151 if not cats:
152 logging.warning('unknown gerrit approval type: %s', approver['type'])
153 continue
154 cat = cats[0].strip()
155 val = int(approver['value'])
156 if not cat in approvs:
157 # Ignore the extended categories in the summary view.
158 continue
159 elif approvs[cat] == '':
160 approvs[cat] = val
161 elif val < 0:
162 approvs[cat] = min(approvs[cat], val)
163 else:
164 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400165 return approvs
166
167
Mike Frysingera1b4b272017-04-05 16:11:00 -0400168def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400169 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400170 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400171 lims = {'url': 0, 'project': 0}
172
173 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400174
175 if opts.verbose:
176 status += '%s ' % (cl['status'],)
177 else:
178 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
179
Mike Frysinger13f23a42013-05-13 17:32:01 -0400180 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400181 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400182 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400183 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400184 functor = lambda x: x
185 elif approvs[cat] < 0:
186 functor = red
187 else:
188 functor = green
189 status += functor('%s:%2s ' % (cat, approvs[cat]))
190
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400191 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
192 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400193
194 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400195 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400196 functor = red if int(approver['value']) < 0 else green
197 n = functor('%2s' % approver['value'])
198 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
199 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500200 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400201
202
Mike Frysingera1b4b272017-04-05 16:11:00 -0400203def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400204 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400205 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600206 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400207 pfx = ''
208 # Special case internal Chrome GoB as that is what most devs use.
209 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600210 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
211 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400212 for cl in cls:
213 print('%s%s' % (pfx, cl['number']))
214
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400215 elif opts.json:
216 json.dump(cls, sys.stdout)
217
Mike Frysingera1b4b272017-04-05 16:11:00 -0400218 else:
219 if lims is None:
220 lims = limits(cls)
221
222 for cl in cls:
223 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
224
225
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400226def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700227 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800228 if opts.branch is not None:
229 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800230 if opts.project is not None:
231 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800232 if opts.topic is not None:
233 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800234
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400235 if helper is None:
236 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700237 return helper.Query(query, raw=raw, bypass_cache=False)
238
239
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400240def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700241 """Query gerrit and filter/clean up the results"""
242 ret = []
243
Mike Frysinger2cd56022017-01-12 20:56:27 -0500244 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400245 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400246 # Gerrit likes to return a stats record too.
247 if not 'project' in cl:
248 continue
249
250 # Strip off common leading names since the result is still
251 # unique over the whole tree.
252 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400253 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
254 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400255 if cl['project'].startswith('%s/' % pfx):
256 cl['project'] = cl['project'][len(pfx) + 1:]
257
Mike Frysinger479f1192017-09-14 22:36:30 -0400258 cl['url'] = uri_lib.ShortenUri(cl['url'])
259
Mike Frysinger13f23a42013-05-13 17:32:01 -0400260 ret.append(cl)
261
Mike Frysingerb62313a2017-06-30 16:38:58 -0400262 if opts.sort == 'unsorted':
263 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700264 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400265 key = lambda x: int(x[opts.sort])
266 else:
267 key = lambda x: x[opts.sort]
268 return sorted(ret, key=key)
269
270
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500271class _ActionSearchQuery(UserAction):
272 """Base class for actions that perform searches."""
273
274 @staticmethod
275 def init_subparser(parser):
276 """Add arguments to this action's subparser."""
277 parser.add_argument('--sort', default='number',
278 help='Key to sort on (number, project); use "unsorted" '
279 'to disable')
280 parser.add_argument('-b', '--branch',
281 help='Limit output to the specific branch')
282 parser.add_argument('-p', '--project',
283 help='Limit output to the specific project')
284 parser.add_argument('-t', '--topic',
285 help='Limit output to the specific topic')
286
287
288class ActionTodo(_ActionSearchQuery):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400289 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500290
291 COMMAND = 'todo'
292
293 @staticmethod
294 def __call__(opts):
295 """Implement the action."""
296 cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
297 'label:Code-Review=0,user=self '
298 'NOT label:Verified<0'))
299 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400300
301
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500302class ActionSearch(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800303 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500304
305 COMMAND = 'search'
306
307 @staticmethod
308 def init_subparser(parser):
309 """Add arguments to this action's subparser."""
310 _ActionSearchQuery.init_subparser(parser)
311 parser.add_argument('query',
312 help='The search query')
313
314 @staticmethod
315 def __call__(opts):
316 """Implement the action."""
317 cls = FilteredQuery(opts, opts.query)
318 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400319
320
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500321class ActionMine(_ActionSearchQuery):
Mike Frysingera1db2c42014-06-15 00:42:48 -0700322 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500323
324 COMMAND = 'mine'
325
326 @staticmethod
327 def init_subparser(parser):
328 """Add arguments to this action's subparser."""
329 _ActionSearchQuery.init_subparser(parser)
330 parser.add_argument('--draft', default=False, action='store_true',
331 help='Show draft changes')
332
333 @staticmethod
334 def __call__(opts):
335 """Implement the action."""
336 if opts.draft:
337 rule = 'is:draft'
338 else:
339 rule = 'status:new'
340 cls = FilteredQuery(opts, 'owner:self %s' % (rule,))
341 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700342
343
Paul Hobbs89765232015-06-24 14:07:49 -0700344def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
345 """Runs breadth first search starting from the nodes in |to_visit|
346
347 Args:
348 to_visit: the starting nodes
349 children: a function which takes a node and returns the nodes adjacent to it
350 visited_key: a function for deduplicating node visits. Defaults to the
351 identity function (lambda x: x)
352
353 Returns:
354 A list of nodes which are reachable from any node in |to_visit| by calling
355 |children| any number of times.
356 """
357 to_visit = list(to_visit)
Mike Frysinger66ce4132019-07-17 22:52:52 -0400358 seen = set(visited_key(x) for x in to_visit)
Paul Hobbs89765232015-06-24 14:07:49 -0700359 for node in to_visit:
360 for child in children(node):
361 key = visited_key(child)
362 if key not in seen:
363 seen.add(key)
364 to_visit.append(child)
365 return to_visit
366
367
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500368class ActionDeps(_ActionSearchQuery):
Paul Hobbs89765232015-06-24 14:07:49 -0700369 """List CLs matching a query, and all transitive dependencies of those CLs"""
Paul Hobbs89765232015-06-24 14:07:49 -0700370
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500371 COMMAND = 'deps'
Paul Hobbs89765232015-06-24 14:07:49 -0700372
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500373 @staticmethod
374 def init_subparser(parser):
375 """Add arguments to this action's subparser."""
376 _ActionSearchQuery.init_subparser(parser)
377 parser.add_argument('query',
378 help='The search query')
379
380 def __call__(self, opts):
381 """Implement the action."""
382 cls = _Query(opts, opts.query, raw=False)
383
384 @memoize.Memoize
385 def _QueryChange(cl, helper=None):
386 return _Query(opts, cl, raw=False, helper=helper)
387
388 transitives = _BreadthFirstSearch(
389 cls, functools.partial(self._Children, opts, _QueryChange),
390 visited_key=lambda cl: cl.gerrit_number)
391
392 transitives_raw = [cl.patch_dict for cl in transitives]
393 PrintCls(opts, transitives_raw)
394
395 @staticmethod
396 def _ProcessDeps(opts, querier, cl, deps, required):
Mike Frysinger5726da92017-09-20 22:14:25 -0400397 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700398 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400399 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400400 if not dep.remote in opts.gerrit:
401 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
402 remote=dep.remote, print_cmd=opts.debug)
403 helper = opts.gerrit[dep.remote]
404
Paul Hobbs89765232015-06-24 14:07:49 -0700405 # TODO(phobbs) this should maybe catch network errors.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500406 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400407
408 # Handle empty results. If we found a commit that was pushed directly
409 # (e.g. a bot commit), then gerrit won't know about it.
410 if not changes:
411 if required:
412 logging.error('CL %s depends on %s which cannot be found',
413 cl, dep.ToGerritQueryText())
414 continue
415
416 # Our query might have matched more than one result. This can come up
417 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
418 # across multiple repos/branches. We blindly check all of them in the
419 # hopes that all open ones are what the user wants, but then again the
420 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
421 if len(changes) > 1:
422 logging.warning('CL %s has an ambiguous CQ dependency %s',
423 cl, dep.ToGerritQueryText())
424 for change in changes:
425 if change.status == 'NEW':
426 yield change
427
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500428 @classmethod
429 def _Children(cls, opts, querier, cl):
Mike Frysinger5726da92017-09-20 22:14:25 -0400430 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500431 for change in cls._ProcessDeps(
432 opts, querier, cl, cl.PaladinDependencies(None), True):
Mike Frysinger5726da92017-09-20 22:14:25 -0400433 yield change
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500434 for change in cls._ProcessDeps(
435 opts, querier, cl, cl.GerritDependencies(), False):
Mike Frysinger5726da92017-09-20 22:14:25 -0400436 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700437
Paul Hobbs89765232015-06-24 14:07:49 -0700438
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500439class ActionInspect(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800440 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500441
442 COMMAND = 'inspect'
443
444 @staticmethod
445 def init_subparser(parser):
446 """Add arguments to this action's subparser."""
447 _ActionSearchQuery.init_subparser(parser)
448 parser.add_argument('cls', nargs='+', metavar='CL',
449 help='The CL(s) to update')
450
451 @staticmethod
452 def __call__(opts):
453 """Implement the action."""
454 cls = []
455 for arg in opts.cls:
456 helper, cl = GetGerrit(opts, arg)
457 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
458 if change:
459 cls.extend(change)
460 else:
461 logging.warning('no results found for CL %s', arg)
462 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400463
464
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500465class _ActionLabeler(UserAction):
466 """Base helper for setting labels."""
467
468 LABEL = None
469 VALUES = None
470
471 @classmethod
472 def init_subparser(cls, parser):
473 """Add arguments to this action's subparser."""
474 parser.add_argument('--ne', '--no-emails', dest='notify',
475 default='ALL', action='store_const', const='NONE',
476 help='Do not send e-mail notifications')
477 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
478 help='Optional message to include')
479 parser.add_argument('cls', nargs='+', metavar='CL',
480 help='The CL(s) to update')
481 parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
482 help='The label value; one of [%(choices)s]')
483
484 @classmethod
485 def __call__(cls, opts):
486 """Implement the action."""
487 # Convert user friendly command line option into a gerrit parameter.
488 def task(arg):
489 helper, cl = GetGerrit(opts, arg)
490 helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
491 dryrun=opts.dryrun, notify=opts.notify)
492 _run_parallel_tasks(task, *opts.cls)
493
494
495class ActionLabelAutoSubmit(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500496 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500497
498 COMMAND = 'label-as'
499 LABEL = 'Auto-Submit'
500 VALUES = ('0', '1')
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600501
502
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500503class ActionLabelCodeReview(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500504 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500505
506 COMMAND = 'label-cr'
507 LABEL = 'Code-Review'
508 VALUES = ('-2', '-1', '0', '1', '2')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400509
510
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500511class ActionLabelVerified(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500512 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500513
514 COMMAND = 'label-v'
515 LABEL = 'Verified'
516 VALUES = ('-1', '0', '1')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400517
518
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500519class ActionLabelCommitQueue(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500520 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500521
522 COMMAND = 'label-cq'
523 LABEL = 'Commit-Queue'
524 VALUES = ('0', '1', '2')
Mike Frysinger15b23e42014-12-05 17:00:05 -0500525
526
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500527class _ActionSimpleParallelCLs(UserAction):
528 """Base helper for actions that only accept CLs."""
529
530 @staticmethod
531 def init_subparser(parser):
532 """Add arguments to this action's subparser."""
533 parser.add_argument('cls', nargs='+', metavar='CL',
534 help='The CL(s) to update')
535
536 def __call__(self, opts):
537 """Implement the action."""
538 def task(arg):
539 helper, cl = GetGerrit(opts, arg)
540 self._process_one(helper, cl, opts)
541 _run_parallel_tasks(task, *opts.cls)
542
543
544class ActionSubmit(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800545 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500546
547 COMMAND = 'submit'
548
549 @staticmethod
550 def _process_one(helper, cl, opts):
551 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700552 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400553
554
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500555class ActionAbandon(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800556 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500557
558 COMMAND = 'abandon'
559
560 @staticmethod
561 def _process_one(helper, cl, opts):
562 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700563 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400564
565
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500566class ActionRestore(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800567 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500568
569 COMMAND = 'restore'
570
571 @staticmethod
572 def _process_one(helper, cl, opts):
573 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700574 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400575
576
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500577class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800578 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700579
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500580 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700581
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500582 @staticmethod
583 def init_subparser(parser):
584 """Add arguments to this action's subparser."""
585 parser.add_argument('--ne', '--no-emails', dest='notify',
586 default='ALL', action='store_const', const='NONE',
587 help='Do not send e-mail notifications')
588 parser.add_argument('cl', metavar='CL',
589 help='The CL to update')
590 parser.add_argument('reviewers', nargs='+',
591 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700592
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500593 @staticmethod
594 def __call__(opts):
595 """Implement the action."""
596 # Allow for optional leading '~'.
597 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
598 add_list, remove_list, invalid_list = [], [], []
599
600 for email in opts.reviewers:
601 if not email_validator.match(email):
602 invalid_list.append(email)
603 elif email[0] == '~':
604 remove_list.append(email[1:])
605 else:
606 add_list.append(email)
607
608 if invalid_list:
609 cros_build_lib.Die(
610 'Invalid email address(es): %s' % ', '.join(invalid_list))
611
612 if add_list or remove_list:
613 helper, cl = GetGerrit(opts, opts.cl)
614 helper.SetReviewers(cl, add=add_list, remove=remove_list,
615 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700616
617
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500618class ActionAssign(_ActionSimpleParallelCLs):
619 """Set the assignee for CLs"""
620
621 COMMAND = 'assign'
622
623 @staticmethod
624 def init_subparser(parser):
625 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400626 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500627 parser.add_argument('assignee',
628 help='The new assignee')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500629
630 @staticmethod
631 def _process_one(helper, cl, opts):
632 """Use |helper| to process the single |cl|."""
633 helper.SetAssignee(cl, opts.assignee, dryrun=opts.dryrun)
Allen Li38abdaa2017-03-16 13:25:02 -0700634
635
Mike Frysinger62178ae2020-03-20 01:37:43 -0400636class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800637 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500638
639 COMMAND = 'message'
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('message',
646 help='The message to post')
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.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530652
653
Mike Frysinger62178ae2020-03-20 01:37:43 -0400654class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800655 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500656
657 COMMAND = 'topic'
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('topic',
664 help='The topic to set')
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.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800670
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800671
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500672class ActionPrivate(_ActionSimpleParallelCLs):
673 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700674
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500675 COMMAND = 'private'
676
677 @staticmethod
678 def _process_one(helper, cl, opts):
679 """Use |helper| to process the single |cl|."""
680 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700681
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800682
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500683class ActionPublic(_ActionSimpleParallelCLs):
684 """Mark CLs public"""
685
686 COMMAND = 'public'
687
688 @staticmethod
689 def _process_one(helper, cl, opts):
690 """Use |helper| to process the single |cl|."""
691 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
692
693
694class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800695 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500696
697 COMMAND = 'hashtags'
698
699 @staticmethod
700 def init_subparser(parser):
701 """Add arguments to this action's subparser."""
702 parser.add_argument('cl', metavar='CL',
703 help='The CL to update')
704 parser.add_argument('hashtags', nargs='+',
705 help='The hashtags to add/remove')
706
707 @staticmethod
708 def __call__(opts):
709 """Implement the action."""
710 add = []
711 remove = []
712 for hashtag in opts.hashtags:
713 if hashtag.startswith('~'):
714 remove.append(hashtag[1:])
715 else:
716 add.append(hashtag)
717 helper, cl = GetGerrit(opts, opts.cl)
718 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800719
720
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500721class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800722 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500723
724 COMMAND = 'deletedraft'
725
726 @staticmethod
727 def _process_one(helper, cl, opts):
728 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700729 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800730
731
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500732class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500733 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500734
735 COMMAND = 'reviewed'
736
737 @staticmethod
738 def _process_one(helper, cl, opts):
739 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500740 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500741
742
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500743class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500744 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500745
746 COMMAND = 'unreviewed'
747
748 @staticmethod
749 def _process_one(helper, cl, opts):
750 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500751 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500752
753
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500754class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500755 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500756
757 COMMAND = 'ignore'
758
759 @staticmethod
760 def _process_one(helper, cl, opts):
761 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500762 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500763
764
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500765class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500766 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500767
768 COMMAND = 'unignore'
769
770 @staticmethod
771 def _process_one(helper, cl, opts):
772 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500773 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500774
775
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500776class ActionAccount(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800777 """Get the current user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500778
779 COMMAND = 'account'
780
781 @staticmethod
782 def __call__(opts):
783 """Implement the action."""
784 helper, _ = GetGerrit(opts)
785 acct = helper.GetAccount()
786 if opts.json:
787 json.dump(acct, sys.stdout)
788 else:
789 print('account_id:%i %s <%s>' %
790 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800791
792
Mike Frysinger65fc8632020-02-06 18:11:12 -0500793@memoize.Memoize
794def _GetActions():
795 """Get all the possible actions we support.
796
797 Returns:
798 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
799 function that implements that command (e.g. UserActFoo).
800 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500801 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
802
803 actions = {}
804 for cls in globals().values():
805 if (not inspect.isclass(cls) or
806 not issubclass(cls, UserAction) or
807 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -0500808 continue
809
Mike Frysinger65fc8632020-02-06 18:11:12 -0500810 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500811 cmd = cls.COMMAND
812 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
813 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -0500814
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500815 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -0500816
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500817 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500818
819
Harry Cutts26076b32019-02-26 15:01:29 -0800820def _GetActionUsages():
821 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -0500822 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -0800823
Mike Frysinger65fc8632020-02-06 18:11:12 -0500824 cmds = list(actions.keys())
825 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -0800826 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500827 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -0800828
Harry Cutts26076b32019-02-26 15:01:29 -0800829 cmd_indent = len(max(cmds, key=len))
830 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500831 return '\n'.join(
832 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
833 for cmd, usage, doc in zip(cmds, usages, docs)
834 )
Harry Cutts26076b32019-02-26 15:01:29 -0800835
836
Mike Frysinger108eda22018-06-06 18:45:12 -0400837def GetParser():
838 """Returns the parser to use for this module."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500839 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -0400840There is no support for doing line-by-line code review via the command line.
841This helps you manage various bits and CL status.
842
Mike Frysingera1db2c42014-06-15 00:42:48 -0700843For general Gerrit documentation, see:
844 https://gerrit-review.googlesource.com/Documentation/
845The Searching Changes page covers the search query syntax:
846 https://gerrit-review.googlesource.com/Documentation/user-search.html
847
Mike Frysinger13f23a42013-05-13 17:32:01 -0400848Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500849 $ gerrit todo # List all the CLs that await your review.
850 $ gerrit mine # List all of your open CLs.
851 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
852 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
853 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800854 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
85528123.
856 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
857CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700858Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500859 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
860with Commit-Queue=1.
861 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
862CLs with Commit-Queue=1.
863 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400864
Harry Cutts26076b32019-02-26 15:01:29 -0800865Actions:
866"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500867 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400868
Mike Frysinger65fc8632020-02-06 18:11:12 -0500869 actions = _GetActions()
870
Alex Klein2ab29cc2018-07-19 12:01:00 -0600871 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -0400872 parser = commandline.ArgumentParser(
873 description=description, default_log_level='notice')
Mike Frysinger08737512014-02-07 22:58:26 -0500874 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600875 default=site_params.EXTERNAL_GOB_INSTANCE,
876 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500877 help='Query internal Chromium Gerrit instance')
878 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600879 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500880 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600881 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700882 parser.add_argument('--raw', default=False, action='store_true',
883 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400884 parser.add_argument('--json', default=False, action='store_true',
885 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700886 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
887 dest='dryrun',
888 help='Show what would be done, but do not make changes')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500889
890 # Subparsers are required by default under Python 2. Python 3 changed to
891 # not required, but didn't include a required option until 3.7. Setting
892 # the required member works in all versions (and setting dest name).
893 subparsers = parser.add_subparsers(dest='action')
894 subparsers.required = True
895 for cmd, cls in actions.items():
896 # Format the full docstring by removing the file level indentation.
897 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
898 subparser = subparsers.add_parser(cmd, description=description)
899 subparser.add_argument('-n', '--dry-run', dest='dryrun',
900 default=False, action='store_true',
901 help='Show what would be done only')
902 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -0400903
904 return parser
905
906
907def main(argv):
908 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500909 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400910
Mike Frysinger88f27292014-06-17 09:40:45 -0700911 # A cache of gerrit helpers we'll load on demand.
912 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800913
Mike Frysinger88f27292014-06-17 09:40:45 -0700914 opts.Freeze()
915
Mike Frysinger27e21b72018-07-12 14:20:21 -0400916 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400917 global COLOR
918 COLOR = terminal.Color(enabled=opts.color)
919
Mike Frysinger13f23a42013-05-13 17:32:01 -0400920 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -0500921 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500922 obj = actions[opts.action]()
Mike Frysinger65fc8632020-02-06 18:11:12 -0500923 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500924 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -0500925 except (cros_build_lib.RunCommandError, gerrit.GerritException,
926 gob_util.GOBError) as e:
927 cros_build_lib.Die(e)