blob: a025dddbcd08bd74d8bfd0947140d3d33f889744 [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 Frysinger108eda22018-06-06 18:45:12 -040051
52
Mike Frysinger254f33f2019-12-11 13:54:29 -050053# How many connections we'll use in parallel. We don't want this to be too high
54# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
55# seems to be good enough for users.
56CONNECTION_LIMIT = 10
57
58
Mike Frysinger031ad0b2013-05-14 18:15:34 -040059COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040060
61# Map the internal names to the ones we normally show on the web ui.
62GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080063 'COMR': ['CQ', 'Commit Queue ',],
64 'CRVW': ['CR', 'Code Review ',],
65 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080066 'VRIF': ['V ', 'Verified ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060067 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040068}
69
70# Order is important -- matches the web ui. This also controls the short
71# entries that we summarize in non-verbose mode.
72GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
73
Mike Frysinger4aea5dc2019-07-17 13:39:56 -040074# Shorter strings for CL status messages.
75GERRIT_SUMMARY_MAP = {
76 'ABANDONED': 'ABD',
77 'MERGED': 'MRG',
78 'NEW': 'NEW',
79 'WIP': 'WIP',
80}
81
Mike Frysinger13f23a42013-05-13 17:32:01 -040082
83def red(s):
84 return COLOR.Color(terminal.Color.RED, s)
85
86
87def green(s):
88 return COLOR.Color(terminal.Color.GREEN, s)
89
90
91def blue(s):
92 return COLOR.Color(terminal.Color.BLUE, s)
93
94
Mike Frysinger254f33f2019-12-11 13:54:29 -050095def _run_parallel_tasks(task, *args):
96 """Small wrapper around BackgroundTaskRunner to enforce job count."""
97 with parallel.BackgroundTaskRunner(task, processes=CONNECTION_LIMIT) as q:
98 for arg in args:
99 q.put([arg])
100
101
Mike Frysinger13f23a42013-05-13 17:32:01 -0400102def limits(cls):
103 """Given a dict of fields, calculate the longest string lengths
104
105 This allows you to easily format the output of many results so that the
106 various cols all line up correctly.
107 """
108 lims = {}
109 for cl in cls:
110 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -0400111 # Use %s rather than str() to avoid codec issues.
112 # We also do this so we can format integers.
113 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400114 return lims
115
116
Mike Frysinger88f27292014-06-17 09:40:45 -0700117# TODO: This func really needs to be merged into the core gerrit logic.
118def GetGerrit(opts, cl=None):
119 """Auto pick the right gerrit instance based on the |cl|
120
121 Args:
122 opts: The general options object.
123 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
124
125 Returns:
126 A tuple of a gerrit object and a sanitized CL #.
127 """
128 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700129 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600130 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600131 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600132 if cl.startswith('*'):
133 cl = cl[1:]
134 else:
135 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700136 elif ':' in cl:
137 gob, cl = cl.split(':', 1)
138
139 if not gob in opts.gerrit:
140 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
141
142 return (opts.gerrit[gob], cl)
143
144
Mike Frysinger13f23a42013-05-13 17:32:01 -0400145def GetApprovalSummary(_opts, cls):
146 """Return a dict of the most important approvals"""
147 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700148 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
149 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
150 if not cats:
151 logging.warning('unknown gerrit approval type: %s', approver['type'])
152 continue
153 cat = cats[0].strip()
154 val = int(approver['value'])
155 if not cat in approvs:
156 # Ignore the extended categories in the summary view.
157 continue
158 elif approvs[cat] == '':
159 approvs[cat] = val
160 elif val < 0:
161 approvs[cat] = min(approvs[cat], val)
162 else:
163 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400164 return approvs
165
166
Mike Frysingera1b4b272017-04-05 16:11:00 -0400167def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400168 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400169 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400170 lims = {'url': 0, 'project': 0}
171
172 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400173
174 if opts.verbose:
175 status += '%s ' % (cl['status'],)
176 else:
177 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
178
Mike Frysinger13f23a42013-05-13 17:32:01 -0400179 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400180 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400181 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400182 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400183 functor = lambda x: x
184 elif approvs[cat] < 0:
185 functor = red
186 else:
187 functor = green
188 status += functor('%s:%2s ' % (cat, approvs[cat]))
189
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400190 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
191 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400192
193 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400194 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400195 functor = red if int(approver['value']) < 0 else green
196 n = functor('%2s' % approver['value'])
197 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
198 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500199 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400200
201
Mike Frysingera1b4b272017-04-05 16:11:00 -0400202def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400203 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400204 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600205 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400206 pfx = ''
207 # Special case internal Chrome GoB as that is what most devs use.
208 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600209 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
210 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400211 for cl in cls:
212 print('%s%s' % (pfx, cl['number']))
213
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400214 elif opts.json:
215 json.dump(cls, sys.stdout)
216
Mike Frysingera1b4b272017-04-05 16:11:00 -0400217 else:
218 if lims is None:
219 lims = limits(cls)
220
221 for cl in cls:
222 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
223
224
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400225def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700226 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800227 if opts.branch is not None:
228 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800229 if opts.project is not None:
230 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800231 if opts.topic is not None:
232 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800233
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400234 if helper is None:
235 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700236 return helper.Query(query, raw=raw, bypass_cache=False)
237
238
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400239def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700240 """Query gerrit and filter/clean up the results"""
241 ret = []
242
Mike Frysinger2cd56022017-01-12 20:56:27 -0500243 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400244 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400245 # Gerrit likes to return a stats record too.
246 if not 'project' in cl:
247 continue
248
249 # Strip off common leading names since the result is still
250 # unique over the whole tree.
251 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400252 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
253 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400254 if cl['project'].startswith('%s/' % pfx):
255 cl['project'] = cl['project'][len(pfx) + 1:]
256
Mike Frysinger479f1192017-09-14 22:36:30 -0400257 cl['url'] = uri_lib.ShortenUri(cl['url'])
258
Mike Frysinger13f23a42013-05-13 17:32:01 -0400259 ret.append(cl)
260
Mike Frysingerb62313a2017-06-30 16:38:58 -0400261 if opts.sort == 'unsorted':
262 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700263 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400264 key = lambda x: int(x[opts.sort])
265 else:
266 key = lambda x: x[opts.sort]
267 return sorted(ret, key=key)
268
269
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500270class _ActionSearchQuery(UserAction):
271 """Base class for actions that perform searches."""
272
273 @staticmethod
274 def init_subparser(parser):
275 """Add arguments to this action's subparser."""
276 parser.add_argument('--sort', default='number',
277 help='Key to sort on (number, project); use "unsorted" '
278 'to disable')
279 parser.add_argument('-b', '--branch',
280 help='Limit output to the specific branch')
281 parser.add_argument('-p', '--project',
282 help='Limit output to the specific project')
283 parser.add_argument('-t', '--topic',
284 help='Limit output to the specific topic')
285
286
287class ActionTodo(_ActionSearchQuery):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400288 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500289
290 COMMAND = 'todo'
291
292 @staticmethod
293 def __call__(opts):
294 """Implement the action."""
295 cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
296 'label:Code-Review=0,user=self '
297 'NOT label:Verified<0'))
298 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),
389 visited_key=lambda cl: cl.gerrit_number)
390
391 transitives_raw = [cl.patch_dict for cl in transitives]
392 PrintCls(opts, transitives_raw)
393
394 @staticmethod
395 def _ProcessDeps(opts, querier, cl, deps, required):
Mike Frysinger5726da92017-09-20 22:14:25 -0400396 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700397 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400398 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400399 if not dep.remote in opts.gerrit:
400 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
401 remote=dep.remote, print_cmd=opts.debug)
402 helper = opts.gerrit[dep.remote]
403
Paul Hobbs89765232015-06-24 14:07:49 -0700404 # TODO(phobbs) this should maybe catch network errors.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500405 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400406
407 # Handle empty results. If we found a commit that was pushed directly
408 # (e.g. a bot commit), then gerrit won't know about it.
409 if not changes:
410 if required:
411 logging.error('CL %s depends on %s which cannot be found',
412 cl, dep.ToGerritQueryText())
413 continue
414
415 # Our query might have matched more than one result. This can come up
416 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
417 # across multiple repos/branches. We blindly check all of them in the
418 # hopes that all open ones are what the user wants, but then again the
419 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
420 if len(changes) > 1:
421 logging.warning('CL %s has an ambiguous CQ dependency %s',
422 cl, dep.ToGerritQueryText())
423 for change in changes:
424 if change.status == 'NEW':
425 yield change
426
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500427 @classmethod
428 def _Children(cls, opts, querier, cl):
Mike Frysinger5726da92017-09-20 22:14:25 -0400429 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500430 for change in cls._ProcessDeps(
431 opts, querier, cl, cl.PaladinDependencies(None), True):
Mike Frysinger5726da92017-09-20 22:14:25 -0400432 yield change
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500433 for change in cls._ProcessDeps(
434 opts, querier, cl, cl.GerritDependencies(), False):
Mike Frysinger5726da92017-09-20 22:14:25 -0400435 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700436
Paul Hobbs89765232015-06-24 14:07:49 -0700437
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500438class ActionInspect(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800439 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500440
441 COMMAND = 'inspect'
442
443 @staticmethod
444 def init_subparser(parser):
445 """Add arguments to this action's subparser."""
446 _ActionSearchQuery.init_subparser(parser)
447 parser.add_argument('cls', nargs='+', metavar='CL',
448 help='The CL(s) to update')
449
450 @staticmethod
451 def __call__(opts):
452 """Implement the action."""
453 cls = []
454 for arg in opts.cls:
455 helper, cl = GetGerrit(opts, arg)
456 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
457 if change:
458 cls.extend(change)
459 else:
460 logging.warning('no results found for CL %s', arg)
461 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400462
463
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500464class _ActionLabeler(UserAction):
465 """Base helper for setting labels."""
466
467 LABEL = None
468 VALUES = None
469
470 @classmethod
471 def init_subparser(cls, parser):
472 """Add arguments to this action's subparser."""
473 parser.add_argument('--ne', '--no-emails', dest='notify',
474 default='ALL', action='store_const', const='NONE',
475 help='Do not send e-mail notifications')
476 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 Frysinger88f27292014-06-17 09:40:45 -0700551 helper.SubmitChange(cl, dryrun=opts.dryrun)
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 Frysinger88f27292014-06-17 09:40:45 -0700562 helper.AbandonChange(cl, dryrun=opts.dryrun)
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
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500576class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800577 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700578
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500579 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700580
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500581 @staticmethod
582 def init_subparser(parser):
583 """Add arguments to this action's subparser."""
584 parser.add_argument('--ne', '--no-emails', dest='notify',
585 default='ALL', action='store_const', const='NONE',
586 help='Do not send e-mail notifications')
587 parser.add_argument('cl', metavar='CL',
588 help='The CL to update')
589 parser.add_argument('reviewers', nargs='+',
590 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700591
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500592 @staticmethod
593 def __call__(opts):
594 """Implement the action."""
595 # Allow for optional leading '~'.
596 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
597 add_list, remove_list, invalid_list = [], [], []
598
599 for email in opts.reviewers:
600 if not email_validator.match(email):
601 invalid_list.append(email)
602 elif email[0] == '~':
603 remove_list.append(email[1:])
604 else:
605 add_list.append(email)
606
607 if invalid_list:
608 cros_build_lib.Die(
609 'Invalid email address(es): %s' % ', '.join(invalid_list))
610
611 if add_list or remove_list:
612 helper, cl = GetGerrit(opts, opts.cl)
613 helper.SetReviewers(cl, add=add_list, remove=remove_list,
614 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700615
616
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500617class ActionAssign(_ActionSimpleParallelCLs):
618 """Set the assignee for CLs"""
619
620 COMMAND = 'assign'
621
622 @staticmethod
623 def init_subparser(parser):
624 """Add arguments to this action's subparser."""
625 parser.add_argument('assignee',
626 help='The new assignee')
627 _ActionSimpleParallelCLs.init_subparser(parser)
628
629 @staticmethod
630 def _process_one(helper, cl, opts):
631 """Use |helper| to process the single |cl|."""
632 helper.SetAssignee(cl, opts.assignee, dryrun=opts.dryrun)
Allen Li38abdaa2017-03-16 13:25:02 -0700633
634
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500635class ActionMessage(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800636 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500637
638 COMMAND = 'message'
639
640 @staticmethod
641 def init_subparser(parser):
642 """Add arguments to this action's subparser."""
643 parser.add_argument('message',
644 help='The message to post')
645 _ActionSimpleParallelCLs.init_subparser(parser)
646
647 @staticmethod
648 def _process_one(helper, cl, opts):
649 """Use |helper| to process the single |cl|."""
650 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530651
652
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500653class ActionTopic(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800654 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500655
656 COMMAND = 'topic'
657
658 @staticmethod
659 def init_subparser(parser):
660 """Add arguments to this action's subparser."""
661 parser.add_argument('topic',
662 help='The topic to set')
663 _ActionSimpleParallelCLs.init_subparser(parser)
664
665 @staticmethod
666 def _process_one(helper, cl, opts):
667 """Use |helper| to process the single |cl|."""
668 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800669
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800670
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500671class ActionPrivate(_ActionSimpleParallelCLs):
672 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700673
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500674 COMMAND = 'private'
675
676 @staticmethod
677 def _process_one(helper, cl, opts):
678 """Use |helper| to process the single |cl|."""
679 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700680
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800681
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500682class ActionPublic(_ActionSimpleParallelCLs):
683 """Mark CLs public"""
684
685 COMMAND = 'public'
686
687 @staticmethod
688 def _process_one(helper, cl, opts):
689 """Use |helper| to process the single |cl|."""
690 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
691
692
693class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800694 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500695
696 COMMAND = 'hashtags'
697
698 @staticmethod
699 def init_subparser(parser):
700 """Add arguments to this action's subparser."""
701 parser.add_argument('cl', metavar='CL',
702 help='The CL to update')
703 parser.add_argument('hashtags', nargs='+',
704 help='The hashtags to add/remove')
705
706 @staticmethod
707 def __call__(opts):
708 """Implement the action."""
709 add = []
710 remove = []
711 for hashtag in opts.hashtags:
712 if hashtag.startswith('~'):
713 remove.append(hashtag[1:])
714 else:
715 add.append(hashtag)
716 helper, cl = GetGerrit(opts, opts.cl)
717 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800718
719
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500720class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800721 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500722
723 COMMAND = 'deletedraft'
724
725 @staticmethod
726 def _process_one(helper, cl, opts):
727 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700728 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800729
730
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500731class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500732 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500733
734 COMMAND = 'reviewed'
735
736 @staticmethod
737 def _process_one(helper, cl, opts):
738 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500739 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500740
741
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500742class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500743 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500744
745 COMMAND = 'unreviewed'
746
747 @staticmethod
748 def _process_one(helper, cl, opts):
749 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500750 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500751
752
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500753class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500754 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500755
756 COMMAND = 'ignore'
757
758 @staticmethod
759 def _process_one(helper, cl, opts):
760 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500761 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500762
763
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500764class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500765 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500766
767 COMMAND = 'unignore'
768
769 @staticmethod
770 def _process_one(helper, cl, opts):
771 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500772 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500773
774
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500775class ActionAccount(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800776 """Get the current user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500777
778 COMMAND = 'account'
779
780 @staticmethod
781 def __call__(opts):
782 """Implement the action."""
783 helper, _ = GetGerrit(opts)
784 acct = helper.GetAccount()
785 if opts.json:
786 json.dump(acct, sys.stdout)
787 else:
788 print('account_id:%i %s <%s>' %
789 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800790
791
Mike Frysinger65fc8632020-02-06 18:11:12 -0500792@memoize.Memoize
793def _GetActions():
794 """Get all the possible actions we support.
795
796 Returns:
797 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
798 function that implements that command (e.g. UserActFoo).
799 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500800 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
801
802 actions = {}
803 for cls in globals().values():
804 if (not inspect.isclass(cls) or
805 not issubclass(cls, UserAction) or
806 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -0500807 continue
808
Mike Frysinger65fc8632020-02-06 18:11:12 -0500809 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500810 cmd = cls.COMMAND
811 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
812 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -0500813
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500814 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -0500815
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500816 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500817
818
Harry Cutts26076b32019-02-26 15:01:29 -0800819def _GetActionUsages():
820 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -0500821 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -0800822
Mike Frysinger65fc8632020-02-06 18:11:12 -0500823 cmds = list(actions.keys())
824 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -0800825 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500826 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -0800827
Harry Cutts26076b32019-02-26 15:01:29 -0800828 cmd_indent = len(max(cmds, key=len))
829 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500830 return '\n'.join(
831 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
832 for cmd, usage, doc in zip(cmds, usages, docs)
833 )
Harry Cutts26076b32019-02-26 15:01:29 -0800834
835
Mike Frysinger108eda22018-06-06 18:45:12 -0400836def GetParser():
837 """Returns the parser to use for this module."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500838 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -0400839There is no support for doing line-by-line code review via the command line.
840This helps you manage various bits and CL status.
841
Mike Frysingera1db2c42014-06-15 00:42:48 -0700842For general Gerrit documentation, see:
843 https://gerrit-review.googlesource.com/Documentation/
844The Searching Changes page covers the search query syntax:
845 https://gerrit-review.googlesource.com/Documentation/user-search.html
846
Mike Frysinger13f23a42013-05-13 17:32:01 -0400847Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500848 $ gerrit todo # List all the CLs that await your review.
849 $ gerrit mine # List all of your open CLs.
850 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
851 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
852 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800853 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
85428123.
855 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
856CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700857Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500858 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
859with Commit-Queue=1.
860 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
861CLs with Commit-Queue=1.
862 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400863
Harry Cutts26076b32019-02-26 15:01:29 -0800864Actions:
865"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500866 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400867
Mike Frysinger65fc8632020-02-06 18:11:12 -0500868 actions = _GetActions()
869
Alex Klein2ab29cc2018-07-19 12:01:00 -0600870 site_params = config_lib.GetSiteParams()
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500871 parser = commandline.ArgumentParser(description=description)
Mike Frysinger08737512014-02-07 22:58:26 -0500872 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600873 default=site_params.EXTERNAL_GOB_INSTANCE,
874 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500875 help='Query internal Chromium Gerrit instance')
876 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600877 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500878 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600879 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700880 parser.add_argument('--raw', default=False, action='store_true',
881 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400882 parser.add_argument('--json', default=False, action='store_true',
883 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700884 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
885 dest='dryrun',
886 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500887 parser.add_argument('-v', '--verbose', default=False, action='store_true',
888 help='Be more verbose in output')
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)