blob: 9bf79079a492b77ddc8b666d3125b97d91c80e61 [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),
Mike Frysingerdc407f52020-05-08 00:34:56 -0400390 visited_key=lambda cl: cl.PatchLink())
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500391
Mike Frysingerdc407f52020-05-08 00:34:56 -0400392 # This is a hack to avoid losing GoB host for each CL. The PrintCls
393 # function assumes the GoB host specified by the user is the only one
394 # that is ever used, but the deps command walks across hosts.
395 if opts.raw:
396 print('\n'.join(x.PatchLink() for x in transitives))
397 else:
398 transitives_raw = [cl.patch_dict for cl in transitives]
399 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500400
401 @staticmethod
402 def _ProcessDeps(opts, querier, cl, deps, required):
Mike Frysinger5726da92017-09-20 22:14:25 -0400403 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700404 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400405 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400406 if not dep.remote in opts.gerrit:
407 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
408 remote=dep.remote, print_cmd=opts.debug)
409 helper = opts.gerrit[dep.remote]
410
Paul Hobbs89765232015-06-24 14:07:49 -0700411 # TODO(phobbs) this should maybe catch network errors.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500412 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400413
414 # Handle empty results. If we found a commit that was pushed directly
415 # (e.g. a bot commit), then gerrit won't know about it.
416 if not changes:
417 if required:
418 logging.error('CL %s depends on %s which cannot be found',
419 cl, dep.ToGerritQueryText())
420 continue
421
422 # Our query might have matched more than one result. This can come up
423 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
424 # across multiple repos/branches. We blindly check all of them in the
425 # hopes that all open ones are what the user wants, but then again the
426 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
427 if len(changes) > 1:
428 logging.warning('CL %s has an ambiguous CQ dependency %s',
429 cl, dep.ToGerritQueryText())
430 for change in changes:
431 if change.status == 'NEW':
432 yield change
433
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500434 @classmethod
435 def _Children(cls, opts, querier, cl):
Mike Frysinger5726da92017-09-20 22:14:25 -0400436 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500437 for change in cls._ProcessDeps(
438 opts, querier, cl, cl.PaladinDependencies(None), True):
Mike Frysinger5726da92017-09-20 22:14:25 -0400439 yield change
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500440 for change in cls._ProcessDeps(
441 opts, querier, cl, cl.GerritDependencies(), False):
Mike Frysinger5726da92017-09-20 22:14:25 -0400442 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700443
Paul Hobbs89765232015-06-24 14:07:49 -0700444
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500445class ActionInspect(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800446 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500447
448 COMMAND = 'inspect'
449
450 @staticmethod
451 def init_subparser(parser):
452 """Add arguments to this action's subparser."""
453 _ActionSearchQuery.init_subparser(parser)
454 parser.add_argument('cls', nargs='+', metavar='CL',
455 help='The CL(s) to update')
456
457 @staticmethod
458 def __call__(opts):
459 """Implement the action."""
460 cls = []
461 for arg in opts.cls:
462 helper, cl = GetGerrit(opts, arg)
463 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
464 if change:
465 cls.extend(change)
466 else:
467 logging.warning('no results found for CL %s', arg)
468 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400469
470
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500471class _ActionLabeler(UserAction):
472 """Base helper for setting labels."""
473
474 LABEL = None
475 VALUES = None
476
477 @classmethod
478 def init_subparser(cls, parser):
479 """Add arguments to this action's subparser."""
480 parser.add_argument('--ne', '--no-emails', dest='notify',
481 default='ALL', action='store_const', const='NONE',
482 help='Do not send e-mail notifications')
483 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
484 help='Optional message to include')
485 parser.add_argument('cls', nargs='+', metavar='CL',
486 help='The CL(s) to update')
487 parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
488 help='The label value; one of [%(choices)s]')
489
490 @classmethod
491 def __call__(cls, opts):
492 """Implement the action."""
493 # Convert user friendly command line option into a gerrit parameter.
494 def task(arg):
495 helper, cl = GetGerrit(opts, arg)
496 helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
497 dryrun=opts.dryrun, notify=opts.notify)
498 _run_parallel_tasks(task, *opts.cls)
499
500
501class ActionLabelAutoSubmit(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500502 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500503
504 COMMAND = 'label-as'
505 LABEL = 'Auto-Submit'
506 VALUES = ('0', '1')
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600507
508
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500509class ActionLabelCodeReview(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500510 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500511
512 COMMAND = 'label-cr'
513 LABEL = 'Code-Review'
514 VALUES = ('-2', '-1', '0', '1', '2')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400515
516
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500517class ActionLabelVerified(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500518 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500519
520 COMMAND = 'label-v'
521 LABEL = 'Verified'
522 VALUES = ('-1', '0', '1')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400523
524
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500525class ActionLabelCommitQueue(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500526 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500527
528 COMMAND = 'label-cq'
529 LABEL = 'Commit-Queue'
530 VALUES = ('0', '1', '2')
Mike Frysinger15b23e42014-12-05 17:00:05 -0500531
532
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500533class _ActionSimpleParallelCLs(UserAction):
534 """Base helper for actions that only accept CLs."""
535
536 @staticmethod
537 def init_subparser(parser):
538 """Add arguments to this action's subparser."""
539 parser.add_argument('cls', nargs='+', metavar='CL',
540 help='The CL(s) to update')
541
542 def __call__(self, opts):
543 """Implement the action."""
544 def task(arg):
545 helper, cl = GetGerrit(opts, arg)
546 self._process_one(helper, cl, opts)
547 _run_parallel_tasks(task, *opts.cls)
548
549
550class ActionSubmit(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800551 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500552
553 COMMAND = 'submit'
554
555 @staticmethod
556 def _process_one(helper, cl, opts):
557 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700558 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400559
560
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500561class ActionAbandon(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800562 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500563
564 COMMAND = 'abandon'
565
566 @staticmethod
567 def _process_one(helper, cl, opts):
568 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700569 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400570
571
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500572class ActionRestore(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800573 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500574
575 COMMAND = 'restore'
576
577 @staticmethod
578 def _process_one(helper, cl, opts):
579 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700580 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400581
582
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500583class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800584 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700585
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500586 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700587
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500588 @staticmethod
589 def init_subparser(parser):
590 """Add arguments to this action's subparser."""
591 parser.add_argument('--ne', '--no-emails', dest='notify',
592 default='ALL', action='store_const', const='NONE',
593 help='Do not send e-mail notifications')
594 parser.add_argument('cl', metavar='CL',
595 help='The CL to update')
596 parser.add_argument('reviewers', nargs='+',
597 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700598
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500599 @staticmethod
600 def __call__(opts):
601 """Implement the action."""
602 # Allow for optional leading '~'.
603 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
604 add_list, remove_list, invalid_list = [], [], []
605
606 for email in opts.reviewers:
607 if not email_validator.match(email):
608 invalid_list.append(email)
609 elif email[0] == '~':
610 remove_list.append(email[1:])
611 else:
612 add_list.append(email)
613
614 if invalid_list:
615 cros_build_lib.Die(
616 'Invalid email address(es): %s' % ', '.join(invalid_list))
617
618 if add_list or remove_list:
619 helper, cl = GetGerrit(opts, opts.cl)
620 helper.SetReviewers(cl, add=add_list, remove=remove_list,
621 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700622
623
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500624class ActionAssign(_ActionSimpleParallelCLs):
625 """Set the assignee for CLs"""
626
627 COMMAND = 'assign'
628
629 @staticmethod
630 def init_subparser(parser):
631 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400632 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500633 parser.add_argument('assignee',
634 help='The new assignee')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500635
636 @staticmethod
637 def _process_one(helper, cl, opts):
638 """Use |helper| to process the single |cl|."""
639 helper.SetAssignee(cl, opts.assignee, dryrun=opts.dryrun)
Allen Li38abdaa2017-03-16 13:25:02 -0700640
641
Mike Frysinger62178ae2020-03-20 01:37:43 -0400642class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800643 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500644
645 COMMAND = 'message'
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('message',
652 help='The message to post')
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.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530658
659
Mike Frysinger62178ae2020-03-20 01:37:43 -0400660class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800661 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500662
663 COMMAND = 'topic'
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('topic',
670 help='The topic to set')
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.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800676
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800677
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500678class ActionPrivate(_ActionSimpleParallelCLs):
679 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700680
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500681 COMMAND = 'private'
682
683 @staticmethod
684 def _process_one(helper, cl, opts):
685 """Use |helper| to process the single |cl|."""
686 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700687
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800688
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500689class ActionPublic(_ActionSimpleParallelCLs):
690 """Mark CLs public"""
691
692 COMMAND = 'public'
693
694 @staticmethod
695 def _process_one(helper, cl, opts):
696 """Use |helper| to process the single |cl|."""
697 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
698
699
700class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800701 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500702
703 COMMAND = 'hashtags'
704
705 @staticmethod
706 def init_subparser(parser):
707 """Add arguments to this action's subparser."""
708 parser.add_argument('cl', metavar='CL',
709 help='The CL to update')
710 parser.add_argument('hashtags', nargs='+',
711 help='The hashtags to add/remove')
712
713 @staticmethod
714 def __call__(opts):
715 """Implement the action."""
716 add = []
717 remove = []
718 for hashtag in opts.hashtags:
719 if hashtag.startswith('~'):
720 remove.append(hashtag[1:])
721 else:
722 add.append(hashtag)
723 helper, cl = GetGerrit(opts, opts.cl)
724 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800725
726
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500727class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800728 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500729
730 COMMAND = 'deletedraft'
731
732 @staticmethod
733 def _process_one(helper, cl, opts):
734 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700735 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800736
737
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500738class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500739 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500740
741 COMMAND = 'reviewed'
742
743 @staticmethod
744 def _process_one(helper, cl, opts):
745 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500746 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500747
748
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500749class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500750 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500751
752 COMMAND = 'unreviewed'
753
754 @staticmethod
755 def _process_one(helper, cl, opts):
756 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500757 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500758
759
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500760class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500761 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500762
763 COMMAND = 'ignore'
764
765 @staticmethod
766 def _process_one(helper, cl, opts):
767 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500768 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500769
770
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500771class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500772 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500773
774 COMMAND = 'unignore'
775
776 @staticmethod
777 def _process_one(helper, cl, opts):
778 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500779 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500780
781
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500782class ActionAccount(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800783 """Get the current user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500784
785 COMMAND = 'account'
786
787 @staticmethod
788 def __call__(opts):
789 """Implement the action."""
790 helper, _ = GetGerrit(opts)
791 acct = helper.GetAccount()
792 if opts.json:
793 json.dump(acct, sys.stdout)
794 else:
795 print('account_id:%i %s <%s>' %
796 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800797
798
Mike Frysinger484e2f82020-03-20 01:41:10 -0400799class ActionHelpAll(UserAction):
800 """Show all actions help output at once."""
801
802 COMMAND = 'help-all'
803
804 @staticmethod
805 def __call__(opts):
806 """Implement the action."""
807 first = True
808 for action in _GetActions():
809 if first:
810 first = False
811 else:
812 print('\n\n')
813
814 try:
815 opts.parser.parse_args([action, '--help'])
816 except SystemExit:
817 pass
818
819
Mike Frysinger65fc8632020-02-06 18:11:12 -0500820@memoize.Memoize
821def _GetActions():
822 """Get all the possible actions we support.
823
824 Returns:
825 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
826 function that implements that command (e.g. UserActFoo).
827 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500828 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
829
830 actions = {}
831 for cls in globals().values():
832 if (not inspect.isclass(cls) or
833 not issubclass(cls, UserAction) or
834 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -0500835 continue
836
Mike Frysinger65fc8632020-02-06 18:11:12 -0500837 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500838 cmd = cls.COMMAND
839 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
840 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -0500841
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500842 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -0500843
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500844 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500845
846
Harry Cutts26076b32019-02-26 15:01:29 -0800847def _GetActionUsages():
848 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -0500849 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -0800850
Mike Frysinger65fc8632020-02-06 18:11:12 -0500851 cmds = list(actions.keys())
852 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -0800853 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500854 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -0800855
Harry Cutts26076b32019-02-26 15:01:29 -0800856 cmd_indent = len(max(cmds, key=len))
857 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500858 return '\n'.join(
859 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
860 for cmd, usage, doc in zip(cmds, usages, docs)
861 )
Harry Cutts26076b32019-02-26 15:01:29 -0800862
863
Mike Frysinger108eda22018-06-06 18:45:12 -0400864def GetParser():
865 """Returns the parser to use for this module."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500866 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -0400867There is no support for doing line-by-line code review via the command line.
868This helps you manage various bits and CL status.
869
Mike Frysingera1db2c42014-06-15 00:42:48 -0700870For general Gerrit documentation, see:
871 https://gerrit-review.googlesource.com/Documentation/
872The Searching Changes page covers the search query syntax:
873 https://gerrit-review.googlesource.com/Documentation/user-search.html
874
Mike Frysinger13f23a42013-05-13 17:32:01 -0400875Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500876 $ gerrit todo # List all the CLs that await your review.
877 $ gerrit mine # List all of your open CLs.
878 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
879 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
880 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800881 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
88228123.
883 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
884CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700885Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500886 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
887with Commit-Queue=1.
888 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
889CLs with Commit-Queue=1.
890 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400891
Harry Cutts26076b32019-02-26 15:01:29 -0800892Actions:
893"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500894 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400895
Mike Frysinger65fc8632020-02-06 18:11:12 -0500896 actions = _GetActions()
897
Alex Klein2ab29cc2018-07-19 12:01:00 -0600898 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -0400899 parser = commandline.ArgumentParser(
900 description=description, default_log_level='notice')
Mike Frysinger08737512014-02-07 22:58:26 -0500901 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600902 default=site_params.EXTERNAL_GOB_INSTANCE,
903 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500904 help='Query internal Chromium Gerrit instance')
905 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600906 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500907 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600908 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700909 parser.add_argument('--raw', default=False, action='store_true',
910 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400911 parser.add_argument('--json', default=False, action='store_true',
912 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700913 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
914 dest='dryrun',
915 help='Show what would be done, but do not make changes')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500916
917 # Subparsers are required by default under Python 2. Python 3 changed to
918 # not required, but didn't include a required option until 3.7. Setting
919 # the required member works in all versions (and setting dest name).
920 subparsers = parser.add_subparsers(dest='action')
921 subparsers.required = True
922 for cmd, cls in actions.items():
923 # Format the full docstring by removing the file level indentation.
924 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
925 subparser = subparsers.add_parser(cmd, description=description)
926 subparser.add_argument('-n', '--dry-run', dest='dryrun',
927 default=False, action='store_true',
928 help='Show what would be done only')
929 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -0400930
931 return parser
932
933
934def main(argv):
935 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500936 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400937
Mike Frysinger484e2f82020-03-20 01:41:10 -0400938 # In case the action wants to throw a parser error.
939 opts.parser = parser
940
Mike Frysinger88f27292014-06-17 09:40:45 -0700941 # A cache of gerrit helpers we'll load on demand.
942 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800943
Mike Frysinger88f27292014-06-17 09:40:45 -0700944 opts.Freeze()
945
Mike Frysinger27e21b72018-07-12 14:20:21 -0400946 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400947 global COLOR
948 COLOR = terminal.Color(enabled=opts.color)
949
Mike Frysinger13f23a42013-05-13 17:32:01 -0400950 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -0500951 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500952 obj = actions[opts.action]()
Mike Frysinger65fc8632020-02-06 18:11:12 -0500953 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500954 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -0500955 except (cros_build_lib.RunCommandError, gerrit.GerritException,
956 gob_util.GOBError) as e:
957 cros_build_lib.Die(e)