blob: bc868255e2412d16727848a4cf1a4f9c337b4a85 [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
Tomasz Figa54d70992021-01-20 13:48:59 +0900583class ActionWorkInProgress(_ActionSimpleParallelCLs):
584 """Mark CLs as work in progress"""
585
586 COMMAND = 'wip'
587
588 @staticmethod
589 def _process_one(helper, cl, opts):
590 """Use |helper| to process the single |cl|."""
591 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
592
593
594class ActionReadyForReview(_ActionSimpleParallelCLs):
595 """Mark CLs as ready for review"""
596
597 COMMAND = 'ready'
598
599 @staticmethod
600 def _process_one(helper, cl, opts):
601 """Use |helper| to process the single |cl|."""
602 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
603
604
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500605class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800606 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700607
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500608 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700609
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500610 @staticmethod
611 def init_subparser(parser):
612 """Add arguments to this action's subparser."""
613 parser.add_argument('--ne', '--no-emails', dest='notify',
614 default='ALL', action='store_const', const='NONE',
615 help='Do not send e-mail notifications')
616 parser.add_argument('cl', metavar='CL',
617 help='The CL to update')
618 parser.add_argument('reviewers', nargs='+',
619 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700620
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500621 @staticmethod
622 def __call__(opts):
623 """Implement the action."""
624 # Allow for optional leading '~'.
625 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
626 add_list, remove_list, invalid_list = [], [], []
627
628 for email in opts.reviewers:
629 if not email_validator.match(email):
630 invalid_list.append(email)
631 elif email[0] == '~':
632 remove_list.append(email[1:])
633 else:
634 add_list.append(email)
635
636 if invalid_list:
637 cros_build_lib.Die(
638 'Invalid email address(es): %s' % ', '.join(invalid_list))
639
640 if add_list or remove_list:
641 helper, cl = GetGerrit(opts, opts.cl)
642 helper.SetReviewers(cl, add=add_list, remove=remove_list,
643 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700644
645
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500646class ActionAssign(_ActionSimpleParallelCLs):
647 """Set the assignee for CLs"""
648
649 COMMAND = 'assign'
650
651 @staticmethod
652 def init_subparser(parser):
653 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400654 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500655 parser.add_argument('assignee',
656 help='The new assignee')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500657
658 @staticmethod
659 def _process_one(helper, cl, opts):
660 """Use |helper| to process the single |cl|."""
661 helper.SetAssignee(cl, opts.assignee, dryrun=opts.dryrun)
Allen Li38abdaa2017-03-16 13:25:02 -0700662
663
Mike Frysinger62178ae2020-03-20 01:37:43 -0400664class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800665 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500666
667 COMMAND = 'message'
668
669 @staticmethod
670 def init_subparser(parser):
671 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400672 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500673 parser.add_argument('message',
674 help='The message to post')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500675
676 @staticmethod
677 def _process_one(helper, cl, opts):
678 """Use |helper| to process the single |cl|."""
679 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530680
681
Mike Frysinger62178ae2020-03-20 01:37:43 -0400682class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800683 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500684
685 COMMAND = 'topic'
686
687 @staticmethod
688 def init_subparser(parser):
689 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400690 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500691 parser.add_argument('topic',
692 help='The topic to set')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500693
694 @staticmethod
695 def _process_one(helper, cl, opts):
696 """Use |helper| to process the single |cl|."""
697 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800698
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800699
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500700class ActionPrivate(_ActionSimpleParallelCLs):
701 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700702
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500703 COMMAND = 'private'
704
705 @staticmethod
706 def _process_one(helper, cl, opts):
707 """Use |helper| to process the single |cl|."""
708 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700709
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800710
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500711class ActionPublic(_ActionSimpleParallelCLs):
712 """Mark CLs public"""
713
714 COMMAND = 'public'
715
716 @staticmethod
717 def _process_one(helper, cl, opts):
718 """Use |helper| to process the single |cl|."""
719 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
720
721
722class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800723 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500724
725 COMMAND = 'hashtags'
726
727 @staticmethod
728 def init_subparser(parser):
729 """Add arguments to this action's subparser."""
730 parser.add_argument('cl', metavar='CL',
731 help='The CL to update')
732 parser.add_argument('hashtags', nargs='+',
733 help='The hashtags to add/remove')
734
735 @staticmethod
736 def __call__(opts):
737 """Implement the action."""
738 add = []
739 remove = []
740 for hashtag in opts.hashtags:
741 if hashtag.startswith('~'):
742 remove.append(hashtag[1:])
743 else:
744 add.append(hashtag)
745 helper, cl = GetGerrit(opts, opts.cl)
746 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800747
748
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500749class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800750 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500751
752 COMMAND = 'deletedraft'
753
754 @staticmethod
755 def _process_one(helper, cl, opts):
756 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700757 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800758
759
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500760class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500761 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500762
763 COMMAND = 'reviewed'
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.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500769
770
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500771class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500772 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500773
774 COMMAND = 'unreviewed'
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.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500780
781
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500782class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500783 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500784
785 COMMAND = 'ignore'
786
787 @staticmethod
788 def _process_one(helper, cl, opts):
789 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500790 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500791
792
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500793class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500794 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500795
796 COMMAND = 'unignore'
797
798 @staticmethod
799 def _process_one(helper, cl, opts):
800 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500801 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500802
803
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500804class ActionAccount(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800805 """Get the current user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500806
807 COMMAND = 'account'
808
809 @staticmethod
810 def __call__(opts):
811 """Implement the action."""
812 helper, _ = GetGerrit(opts)
813 acct = helper.GetAccount()
814 if opts.json:
815 json.dump(acct, sys.stdout)
816 else:
817 print('account_id:%i %s <%s>' %
818 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800819
820
Mike Frysinger484e2f82020-03-20 01:41:10 -0400821class ActionHelpAll(UserAction):
822 """Show all actions help output at once."""
823
824 COMMAND = 'help-all'
825
826 @staticmethod
827 def __call__(opts):
828 """Implement the action."""
829 first = True
830 for action in _GetActions():
831 if first:
832 first = False
833 else:
834 print('\n\n')
835
836 try:
837 opts.parser.parse_args([action, '--help'])
838 except SystemExit:
839 pass
840
841
Mike Frysinger65fc8632020-02-06 18:11:12 -0500842@memoize.Memoize
843def _GetActions():
844 """Get all the possible actions we support.
845
846 Returns:
847 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
848 function that implements that command (e.g. UserActFoo).
849 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500850 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
851
852 actions = {}
853 for cls in globals().values():
854 if (not inspect.isclass(cls) or
855 not issubclass(cls, UserAction) or
856 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -0500857 continue
858
Mike Frysinger65fc8632020-02-06 18:11:12 -0500859 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500860 cmd = cls.COMMAND
861 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
862 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -0500863
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500864 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -0500865
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500866 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500867
868
Harry Cutts26076b32019-02-26 15:01:29 -0800869def _GetActionUsages():
870 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -0500871 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -0800872
Mike Frysinger65fc8632020-02-06 18:11:12 -0500873 cmds = list(actions.keys())
874 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -0800875 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500876 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -0800877
Harry Cutts26076b32019-02-26 15:01:29 -0800878 cmd_indent = len(max(cmds, key=len))
879 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500880 return '\n'.join(
881 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
882 for cmd, usage, doc in zip(cmds, usages, docs)
883 )
Harry Cutts26076b32019-02-26 15:01:29 -0800884
885
Mike Frysinger108eda22018-06-06 18:45:12 -0400886def GetParser():
887 """Returns the parser to use for this module."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500888 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -0400889There is no support for doing line-by-line code review via the command line.
890This helps you manage various bits and CL status.
891
Mike Frysingera1db2c42014-06-15 00:42:48 -0700892For general Gerrit documentation, see:
893 https://gerrit-review.googlesource.com/Documentation/
894The Searching Changes page covers the search query syntax:
895 https://gerrit-review.googlesource.com/Documentation/user-search.html
896
Mike Frysinger13f23a42013-05-13 17:32:01 -0400897Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500898 $ gerrit todo # List all the CLs that await your review.
899 $ gerrit mine # List all of your open CLs.
900 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
901 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
902 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800903 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
90428123.
905 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
906CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700907Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500908 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
909with Commit-Queue=1.
910 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
911CLs with Commit-Queue=1.
912 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400913
Harry Cutts26076b32019-02-26 15:01:29 -0800914Actions:
915"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500916 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400917
Mike Frysinger65fc8632020-02-06 18:11:12 -0500918 actions = _GetActions()
919
Alex Klein2ab29cc2018-07-19 12:01:00 -0600920 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -0400921 parser = commandline.ArgumentParser(
922 description=description, default_log_level='notice')
Mike Frysinger08737512014-02-07 22:58:26 -0500923 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600924 default=site_params.EXTERNAL_GOB_INSTANCE,
925 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500926 help='Query internal Chromium Gerrit instance')
927 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600928 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500929 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600930 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700931 parser.add_argument('--raw', default=False, action='store_true',
932 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400933 parser.add_argument('--json', default=False, action='store_true',
934 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700935 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
936 dest='dryrun',
937 help='Show what would be done, but do not make changes')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500938
939 # Subparsers are required by default under Python 2. Python 3 changed to
940 # not required, but didn't include a required option until 3.7. Setting
941 # the required member works in all versions (and setting dest name).
942 subparsers = parser.add_subparsers(dest='action')
943 subparsers.required = True
944 for cmd, cls in actions.items():
945 # Format the full docstring by removing the file level indentation.
946 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
947 subparser = subparsers.add_parser(cmd, description=description)
948 subparser.add_argument('-n', '--dry-run', dest='dryrun',
949 default=False, action='store_true',
950 help='Show what would be done only')
951 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -0400952
953 return parser
954
955
956def main(argv):
957 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500958 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400959
Mike Frysinger484e2f82020-03-20 01:41:10 -0400960 # In case the action wants to throw a parser error.
961 opts.parser = parser
962
Mike Frysinger88f27292014-06-17 09:40:45 -0700963 # A cache of gerrit helpers we'll load on demand.
964 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800965
Mike Frysinger88f27292014-06-17 09:40:45 -0700966 opts.Freeze()
967
Mike Frysinger27e21b72018-07-12 14:20:21 -0400968 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400969 global COLOR
970 COLOR = terminal.Color(enabled=opts.color)
971
Mike Frysinger13f23a42013-05-13 17:32:01 -0400972 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -0500973 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500974 obj = actions[opts.action]()
Mike Frysinger65fc8632020-02-06 18:11:12 -0500975 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500976 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -0500977 except (cros_build_lib.RunCommandError, gerrit.GerritException,
978 gob_util.GOBError) as e:
979 cros_build_lib.Die(e)