blob: acfbb0e9999ad3b7a28d7ed5e5b353c5ad2d6af6 [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 Frysinger8037f752020-02-29 20:47:09 -050015import argparse
Mike Frysinger65fc8632020-02-06 18:11:12 -050016import collections
Mike Frysingerc7796cf2020-02-06 23:55:15 -050017import functools
Mike Frysinger13f23a42013-05-13 17:32:01 -040018import inspect
Mike Frysinger87c74ce2017-04-04 16:12:31 -040019import json
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070020import re
Mike Frysinger87c74ce2017-04-04 16:12:31 -040021import sys
Mike Frysinger13f23a42013-05-13 17:32:01 -040022
Aviv Keshetb7519e12016-10-04 00:50:00 -070023from chromite.lib import config_lib
24from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040025from chromite.lib import commandline
26from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070027from chromite.lib import cros_logging as logging
Mike Frysinger13f23a42013-05-13 17:32:01 -040028from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050029from chromite.lib import gob_util
Mike Frysinger254f33f2019-12-11 13:54:29 -050030from chromite.lib import parallel
Mike Frysinger7f2018d2021-02-04 00:10:58 -050031from chromite.lib import pformat
Mike Frysinger13f23a42013-05-13 17:32:01 -040032from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040033from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060034from chromite.utils import memoize
Mike Frysinger13f23a42013-05-13 17:32:01 -040035
36
Mike Frysinger1c76d4c2020-02-08 23:35:29 -050037assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
38
39
Mike Frysingerc7796cf2020-02-06 23:55:15 -050040class UserAction(object):
41 """Base class for all custom user actions."""
42
43 # The name of the command the user types in.
44 COMMAND = None
45
46 @staticmethod
47 def init_subparser(parser):
48 """Add arguments to this action's subparser."""
49
50 @staticmethod
51 def __call__(opts):
52 """Implement the action."""
Mike Frysinger62178ae2020-03-20 01:37:43 -040053 raise RuntimeError('Internal error: action missing __call__ implementation')
Mike Frysinger108eda22018-06-06 18:45:12 -040054
55
Mike Frysinger254f33f2019-12-11 13:54:29 -050056# How many connections we'll use in parallel. We don't want this to be too high
57# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
58# seems to be good enough for users.
59CONNECTION_LIMIT = 10
60
61
Mike Frysinger031ad0b2013-05-14 18:15:34 -040062COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040063
64# Map the internal names to the ones we normally show on the web ui.
65GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080066 'COMR': ['CQ', 'Commit Queue ',],
67 'CRVW': ['CR', 'Code Review ',],
68 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080069 'VRIF': ['V ', 'Verified ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060070 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040071}
72
73# Order is important -- matches the web ui. This also controls the short
74# entries that we summarize in non-verbose mode.
75GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
76
Mike Frysinger4aea5dc2019-07-17 13:39:56 -040077# Shorter strings for CL status messages.
78GERRIT_SUMMARY_MAP = {
79 'ABANDONED': 'ABD',
80 'MERGED': 'MRG',
81 'NEW': 'NEW',
82 'WIP': 'WIP',
83}
84
Mike Frysinger13f23a42013-05-13 17:32:01 -040085
86def red(s):
87 return COLOR.Color(terminal.Color.RED, s)
88
89
90def green(s):
91 return COLOR.Color(terminal.Color.GREEN, s)
92
93
94def blue(s):
95 return COLOR.Color(terminal.Color.BLUE, s)
96
97
Mike Frysinger254f33f2019-12-11 13:54:29 -050098def _run_parallel_tasks(task, *args):
99 """Small wrapper around BackgroundTaskRunner to enforce job count."""
100 with parallel.BackgroundTaskRunner(task, processes=CONNECTION_LIMIT) as q:
101 for arg in args:
102 q.put([arg])
103
104
Mike Frysinger13f23a42013-05-13 17:32:01 -0400105def limits(cls):
106 """Given a dict of fields, calculate the longest string lengths
107
108 This allows you to easily format the output of many results so that the
109 various cols all line up correctly.
110 """
111 lims = {}
112 for cl in cls:
113 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -0400114 # Use %s rather than str() to avoid codec issues.
115 # We also do this so we can format integers.
116 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400117 return lims
118
119
Mike Frysinger88f27292014-06-17 09:40:45 -0700120# TODO: This func really needs to be merged into the core gerrit logic.
121def GetGerrit(opts, cl=None):
122 """Auto pick the right gerrit instance based on the |cl|
123
124 Args:
125 opts: The general options object.
126 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
127
128 Returns:
129 A tuple of a gerrit object and a sanitized CL #.
130 """
131 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700132 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600133 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600134 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600135 if cl.startswith('*'):
136 cl = cl[1:]
137 else:
138 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700139 elif ':' in cl:
140 gob, cl = cl.split(':', 1)
141
142 if not gob in opts.gerrit:
143 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
144
145 return (opts.gerrit[gob], cl)
146
147
Mike Frysinger13f23a42013-05-13 17:32:01 -0400148def GetApprovalSummary(_opts, cls):
149 """Return a dict of the most important approvals"""
150 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700151 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
152 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
153 if not cats:
154 logging.warning('unknown gerrit approval type: %s', approver['type'])
155 continue
156 cat = cats[0].strip()
157 val = int(approver['value'])
158 if not cat in approvs:
159 # Ignore the extended categories in the summary view.
160 continue
161 elif approvs[cat] == '':
162 approvs[cat] = val
163 elif val < 0:
164 approvs[cat] = min(approvs[cat], val)
165 else:
166 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400167 return approvs
168
169
Mike Frysingera1b4b272017-04-05 16:11:00 -0400170def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400171 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400172 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400173 lims = {'url': 0, 'project': 0}
174
175 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400176
177 if opts.verbose:
178 status += '%s ' % (cl['status'],)
179 else:
180 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
181
Mike Frysinger13f23a42013-05-13 17:32:01 -0400182 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400183 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400184 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400185 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400186 functor = lambda x: x
187 elif approvs[cat] < 0:
188 functor = red
189 else:
190 functor = green
191 status += functor('%s:%2s ' % (cat, approvs[cat]))
192
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400193 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
194 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400195
196 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400197 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400198 functor = red if int(approver['value']) < 0 else green
199 n = functor('%2s' % approver['value'])
200 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
201 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500202 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400203
204
Mike Frysingera1b4b272017-04-05 16:11:00 -0400205def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400206 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400207 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600208 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400209 pfx = ''
210 # Special case internal Chrome GoB as that is what most devs use.
211 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600212 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
213 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400214 for cl in cls:
215 print('%s%s' % (pfx, cl['number']))
216
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400217 elif opts.json:
218 json.dump(cls, sys.stdout)
219
Mike Frysingera1b4b272017-04-05 16:11:00 -0400220 else:
221 if lims is None:
222 lims = limits(cls)
223
224 for cl in cls:
225 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
226
227
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400228def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700229 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800230 if opts.branch is not None:
231 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800232 if opts.project is not None:
233 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800234 if opts.topic is not None:
235 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800236
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400237 if helper is None:
238 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700239 return helper.Query(query, raw=raw, bypass_cache=False)
240
241
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400242def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700243 """Query gerrit and filter/clean up the results"""
244 ret = []
245
Mike Frysinger2cd56022017-01-12 20:56:27 -0500246 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400247 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400248 # Gerrit likes to return a stats record too.
249 if not 'project' in cl:
250 continue
251
252 # Strip off common leading names since the result is still
253 # unique over the whole tree.
254 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400255 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
256 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400257 if cl['project'].startswith('%s/' % pfx):
258 cl['project'] = cl['project'][len(pfx) + 1:]
259
Mike Frysinger479f1192017-09-14 22:36:30 -0400260 cl['url'] = uri_lib.ShortenUri(cl['url'])
261
Mike Frysinger13f23a42013-05-13 17:32:01 -0400262 ret.append(cl)
263
Mike Frysingerb62313a2017-06-30 16:38:58 -0400264 if opts.sort == 'unsorted':
265 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700266 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400267 key = lambda x: int(x[opts.sort])
268 else:
269 key = lambda x: x[opts.sort]
270 return sorted(ret, key=key)
271
272
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500273class _ActionSearchQuery(UserAction):
274 """Base class for actions that perform searches."""
275
276 @staticmethod
277 def init_subparser(parser):
278 """Add arguments to this action's subparser."""
279 parser.add_argument('--sort', default='number',
280 help='Key to sort on (number, project); use "unsorted" '
281 'to disable')
282 parser.add_argument('-b', '--branch',
283 help='Limit output to the specific branch')
284 parser.add_argument('-p', '--project',
285 help='Limit output to the specific project')
286 parser.add_argument('-t', '--topic',
287 help='Limit output to the specific topic')
288
289
290class ActionTodo(_ActionSearchQuery):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400291 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500292
293 COMMAND = 'todo'
294
295 @staticmethod
296 def __call__(opts):
297 """Implement the action."""
Mike Frysinger242d2922021-02-09 14:31:50 -0500298 cls = FilteredQuery(opts, 'attention:self')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500299 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 Frysinger7cbd88c2021-02-12 03:52:25 -0500436 """Yields the Gerrit dependencies of a patch"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500437 for change in cls._ProcessDeps(
438 opts, querier, cl, cl.GerritDependencies(), False):
Mike Frysinger5726da92017-09-20 22:14:25 -0400439 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700440
Paul Hobbs89765232015-06-24 14:07:49 -0700441
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500442class ActionInspect(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800443 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500444
445 COMMAND = 'inspect'
446
447 @staticmethod
448 def init_subparser(parser):
449 """Add arguments to this action's subparser."""
450 _ActionSearchQuery.init_subparser(parser)
451 parser.add_argument('cls', nargs='+', metavar='CL',
452 help='The CL(s) to update')
453
454 @staticmethod
455 def __call__(opts):
456 """Implement the action."""
457 cls = []
458 for arg in opts.cls:
459 helper, cl = GetGerrit(opts, arg)
460 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
461 if change:
462 cls.extend(change)
463 else:
464 logging.warning('no results found for CL %s', arg)
465 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400466
467
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500468class _ActionLabeler(UserAction):
469 """Base helper for setting labels."""
470
471 LABEL = None
472 VALUES = None
473
474 @classmethod
475 def init_subparser(cls, parser):
476 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500477 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
478 help='Optional message to include')
479 parser.add_argument('cls', nargs='+', metavar='CL',
480 help='The CL(s) to update')
481 parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
482 help='The label value; one of [%(choices)s]')
483
484 @classmethod
485 def __call__(cls, opts):
486 """Implement the action."""
487 # Convert user friendly command line option into a gerrit parameter.
488 def task(arg):
489 helper, cl = GetGerrit(opts, arg)
490 helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
491 dryrun=opts.dryrun, notify=opts.notify)
492 _run_parallel_tasks(task, *opts.cls)
493
494
495class ActionLabelAutoSubmit(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500496 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500497
498 COMMAND = 'label-as'
499 LABEL = 'Auto-Submit'
500 VALUES = ('0', '1')
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600501
502
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500503class ActionLabelCodeReview(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500504 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500505
506 COMMAND = 'label-cr'
507 LABEL = 'Code-Review'
508 VALUES = ('-2', '-1', '0', '1', '2')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400509
510
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500511class ActionLabelVerified(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500512 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500513
514 COMMAND = 'label-v'
515 LABEL = 'Verified'
516 VALUES = ('-1', '0', '1')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400517
518
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500519class ActionLabelCommitQueue(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500520 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500521
522 COMMAND = 'label-cq'
523 LABEL = 'Commit-Queue'
524 VALUES = ('0', '1', '2')
Mike Frysinger15b23e42014-12-05 17:00:05 -0500525
526
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500527class _ActionSimpleParallelCLs(UserAction):
528 """Base helper for actions that only accept CLs."""
529
530 @staticmethod
531 def init_subparser(parser):
532 """Add arguments to this action's subparser."""
533 parser.add_argument('cls', nargs='+', metavar='CL',
534 help='The CL(s) to update')
535
536 def __call__(self, opts):
537 """Implement the action."""
538 def task(arg):
539 helper, cl = GetGerrit(opts, arg)
540 self._process_one(helper, cl, opts)
541 _run_parallel_tasks(task, *opts.cls)
542
543
544class ActionSubmit(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800545 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500546
547 COMMAND = 'submit'
548
549 @staticmethod
550 def _process_one(helper, cl, opts):
551 """Use |helper| to process the single |cl|."""
Mike Frysinger8674a112021-02-09 14:44:17 -0500552 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400553
554
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500555class ActionAbandon(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800556 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500557
558 COMMAND = 'abandon'
559
560 @staticmethod
561 def _process_one(helper, cl, opts):
562 """Use |helper| to process the single |cl|."""
Mike Frysinger8674a112021-02-09 14:44:17 -0500563 helper.AbandonChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400564
565
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500566class ActionRestore(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800567 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500568
569 COMMAND = 'restore'
570
571 @staticmethod
572 def _process_one(helper, cl, opts):
573 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700574 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400575
576
Tomasz Figa54d70992021-01-20 13:48:59 +0900577class ActionWorkInProgress(_ActionSimpleParallelCLs):
578 """Mark CLs as work in progress"""
579
580 COMMAND = 'wip'
581
582 @staticmethod
583 def _process_one(helper, cl, opts):
584 """Use |helper| to process the single |cl|."""
585 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
586
587
588class ActionReadyForReview(_ActionSimpleParallelCLs):
589 """Mark CLs as ready for review"""
590
591 COMMAND = 'ready'
592
593 @staticmethod
594 def _process_one(helper, cl, opts):
595 """Use |helper| to process the single |cl|."""
596 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
597
598
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500599class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800600 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700601
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500602 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700603
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500604 @staticmethod
605 def init_subparser(parser):
606 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500607 parser.add_argument('cl', metavar='CL',
608 help='The CL to update')
609 parser.add_argument('reviewers', nargs='+',
610 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700611
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500612 @staticmethod
613 def __call__(opts):
614 """Implement the action."""
615 # Allow for optional leading '~'.
616 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
617 add_list, remove_list, invalid_list = [], [], []
618
619 for email in opts.reviewers:
620 if not email_validator.match(email):
621 invalid_list.append(email)
622 elif email[0] == '~':
623 remove_list.append(email[1:])
624 else:
625 add_list.append(email)
626
627 if invalid_list:
628 cros_build_lib.Die(
629 'Invalid email address(es): %s' % ', '.join(invalid_list))
630
631 if add_list or remove_list:
632 helper, cl = GetGerrit(opts, opts.cl)
633 helper.SetReviewers(cl, add=add_list, remove=remove_list,
634 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700635
636
Mike Frysinger62178ae2020-03-20 01:37:43 -0400637class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800638 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500639
640 COMMAND = 'message'
641
642 @staticmethod
643 def init_subparser(parser):
644 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400645 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500646 parser.add_argument('message',
647 help='The message to post')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500648
649 @staticmethod
650 def _process_one(helper, cl, opts):
651 """Use |helper| to process the single |cl|."""
652 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530653
654
Mike Frysinger62178ae2020-03-20 01:37:43 -0400655class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800656 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500657
658 COMMAND = 'topic'
659
660 @staticmethod
661 def init_subparser(parser):
662 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400663 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500664 parser.add_argument('topic',
665 help='The topic to set')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500666
667 @staticmethod
668 def _process_one(helper, cl, opts):
669 """Use |helper| to process the single |cl|."""
670 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800671
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800672
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500673class ActionPrivate(_ActionSimpleParallelCLs):
674 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700675
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500676 COMMAND = 'private'
677
678 @staticmethod
679 def _process_one(helper, cl, opts):
680 """Use |helper| to process the single |cl|."""
681 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700682
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800683
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500684class ActionPublic(_ActionSimpleParallelCLs):
685 """Mark CLs public"""
686
687 COMMAND = 'public'
688
689 @staticmethod
690 def _process_one(helper, cl, opts):
691 """Use |helper| to process the single |cl|."""
692 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
693
694
695class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800696 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500697
698 COMMAND = 'hashtags'
699
700 @staticmethod
701 def init_subparser(parser):
702 """Add arguments to this action's subparser."""
703 parser.add_argument('cl', metavar='CL',
704 help='The CL to update')
705 parser.add_argument('hashtags', nargs='+',
706 help='The hashtags to add/remove')
707
708 @staticmethod
709 def __call__(opts):
710 """Implement the action."""
711 add = []
712 remove = []
713 for hashtag in opts.hashtags:
714 if hashtag.startswith('~'):
715 remove.append(hashtag[1:])
716 else:
717 add.append(hashtag)
718 helper, cl = GetGerrit(opts, opts.cl)
719 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800720
721
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500722class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800723 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500724
725 COMMAND = 'deletedraft'
726
727 @staticmethod
728 def _process_one(helper, cl, opts):
729 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700730 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800731
732
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500733class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500734 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500735
736 COMMAND = 'reviewed'
737
738 @staticmethod
739 def _process_one(helper, cl, opts):
740 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500741 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500742
743
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500744class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500745 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500746
747 COMMAND = 'unreviewed'
748
749 @staticmethod
750 def _process_one(helper, cl, opts):
751 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500752 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500753
754
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500755class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500756 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500757
758 COMMAND = 'ignore'
759
760 @staticmethod
761 def _process_one(helper, cl, opts):
762 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500763 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500764
765
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500766class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500767 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500768
769 COMMAND = 'unignore'
770
771 @staticmethod
772 def _process_one(helper, cl, opts):
773 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500774 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500775
776
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400777class ActionCherryPick(UserAction):
778 """Cherry pick CLs to branches."""
779
780 COMMAND = 'cherry-pick'
781
782 @staticmethod
783 def init_subparser(parser):
784 """Add arguments to this action's subparser."""
785 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
786 parser.add_argument('--rev', '--revision', default='current',
787 help='A specific revision or patchset')
788 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
789 help='Include a message')
790 parser.add_argument('--branches', '--branch', '--br', action='split_extend',
791 default=[], required=True,
792 help='The destination branches')
793 parser.add_argument('cls', nargs='+', metavar='CL',
794 help='The CLs to cherry-pick')
795
796 @staticmethod
797 def __call__(opts):
798 """Implement the action."""
799 # Process branches in parallel, but CLs in serial in case of CL stacks.
800 def task(branch):
801 for arg in opts.cls:
802 helper, cl = GetGerrit(opts, arg)
803 ret = helper.CherryPick(cl, branch, rev=opts.rev, msg=opts.msg,
Mike Frysinger8674a112021-02-09 14:44:17 -0500804 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400805 logging.debug('Response: %s', ret)
806 if opts.raw:
807 print(ret['_number'])
808 else:
809 uri = f'https://{helper.host}/c/{ret["_number"]}'
810 print(uri_lib.ShortenUri(uri))
811
812 _run_parallel_tasks(task, *opts.branches)
813
814
Mike Frysinger8037f752020-02-29 20:47:09 -0500815class ActionReview(_ActionSimpleParallelCLs):
816 """Review CLs with multiple settings
817
818 The label option supports extended/multiple syntax for easy use. The --label
819 option may be specified multiple times (as settings are merges), and multiple
820 labels are allowed in a single argument. Each label has the form:
821 <long or short name><=+-><value>
822
823 Common arguments:
824 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
825 'V+1 CQ+2'
826 'AS=1 V=1'
827 """
828
829 COMMAND = 'review'
830
831 class _SetLabel(argparse.Action):
832 """Argparse action for setting labels."""
833
834 LABEL_MAP = {
835 'AS': 'Auto-Submit',
836 'CQ': 'Commit-Queue',
837 'CR': 'Code-Review',
838 'V': 'Verified',
839 }
840
841 def __call__(self, parser, namespace, values, option_string=None):
842 labels = getattr(namespace, self.dest)
843 for request in values.split():
844 if '=' in request:
845 # Handle Verified=1 form.
846 short, value = request.split('=', 1)
847 elif '+' in request:
848 # Handle Verified+1 form.
849 short, value = request.split('+', 1)
850 elif '-' in request:
851 # Handle Verified-1 form.
852 short, value = request.split('-', 1)
853 value = '-%s' % (value,)
854 else:
855 parser.error('Invalid label setting "%s". Must be Commit-Queue=1 or '
856 'CQ+1 or CR-1.' % (request,))
857
858 # Convert possible short label names like "V" to "Verified".
859 label = self.LABEL_MAP.get(short)
860 if not label:
861 label = short
862
863 # We allow existing label requests to be overridden.
864 labels[label] = value
865
866 @classmethod
867 def init_subparser(cls, parser):
868 """Add arguments to this action's subparser."""
869 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
870 help='Include a message')
871 parser.add_argument('-l', '--label', dest='labels',
872 action=cls._SetLabel, default={},
873 help='Set a label with a value')
874 parser.add_argument('--ready', default=None, action='store_true',
875 help='Set CL status to ready-for-review')
876 parser.add_argument('--wip', default=None, action='store_true',
877 help='Set CL status to WIP')
878 parser.add_argument('--reviewers', '--re', action='append', default=[],
879 help='Add reviewers')
880 parser.add_argument('--cc', action='append', default=[],
881 help='Add people to CC')
882 _ActionSimpleParallelCLs.init_subparser(parser)
883
884 @staticmethod
885 def _process_one(helper, cl, opts):
886 """Use |helper| to process the single |cl|."""
887 helper.SetReview(cl, msg=opts.msg, labels=opts.labels, dryrun=opts.dryrun,
888 notify=opts.notify, reviewers=opts.reviewers, cc=opts.cc,
889 ready=opts.ready, wip=opts.wip)
890
891
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500892class ActionAccount(_ActionSimpleParallelCLs):
893 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500894
895 COMMAND = 'account'
896
897 @staticmethod
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500898 def init_subparser(parser):
899 """Add arguments to this action's subparser."""
900 parser.add_argument('accounts', nargs='*', default=['self'],
901 help='The accounts to query')
902
903 @classmethod
904 def __call__(cls, opts):
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500905 """Implement the action."""
906 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500907
908 def print_one(header, data):
909 print(f'### {header}')
910 print(pformat.json(data, compact=opts.json).rstrip())
911
912 def task(arg):
913 detail = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/detail')
914 if not detail:
915 print(f'{arg}: account not found')
916 else:
917 print_one('detail', detail)
918 for field in ('groups', 'capabilities', 'preferences', 'sshkeys',
919 'gpgkeys'):
920 data = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/{field}')
921 print_one(field, data)
922
923 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800924
925
Mike Frysinger484e2f82020-03-20 01:41:10 -0400926class ActionHelpAll(UserAction):
927 """Show all actions help output at once."""
928
929 COMMAND = 'help-all'
930
931 @staticmethod
932 def __call__(opts):
933 """Implement the action."""
934 first = True
935 for action in _GetActions():
936 if first:
937 first = False
938 else:
939 print('\n\n')
940
941 try:
942 opts.parser.parse_args([action, '--help'])
943 except SystemExit:
944 pass
945
946
Mike Frysinger65fc8632020-02-06 18:11:12 -0500947@memoize.Memoize
948def _GetActions():
949 """Get all the possible actions we support.
950
951 Returns:
952 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
953 function that implements that command (e.g. UserActFoo).
954 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500955 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
956
957 actions = {}
958 for cls in globals().values():
959 if (not inspect.isclass(cls) or
960 not issubclass(cls, UserAction) or
961 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -0500962 continue
963
Mike Frysinger65fc8632020-02-06 18:11:12 -0500964 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500965 cmd = cls.COMMAND
966 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
967 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -0500968
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500969 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -0500970
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500971 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500972
973
Harry Cutts26076b32019-02-26 15:01:29 -0800974def _GetActionUsages():
975 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -0500976 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -0800977
Mike Frysinger65fc8632020-02-06 18:11:12 -0500978 cmds = list(actions.keys())
979 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -0800980 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500981 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -0800982
Harry Cutts26076b32019-02-26 15:01:29 -0800983 cmd_indent = len(max(cmds, key=len))
984 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500985 return '\n'.join(
986 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
987 for cmd, usage, doc in zip(cmds, usages, docs)
988 )
Harry Cutts26076b32019-02-26 15:01:29 -0800989
990
Mike Frysinger108eda22018-06-06 18:45:12 -0400991def GetParser():
992 """Returns the parser to use for this module."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500993 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -0400994There is no support for doing line-by-line code review via the command line.
995This helps you manage various bits and CL status.
996
Mike Frysingera1db2c42014-06-15 00:42:48 -0700997For general Gerrit documentation, see:
998 https://gerrit-review.googlesource.com/Documentation/
999The Searching Changes page covers the search query syntax:
1000 https://gerrit-review.googlesource.com/Documentation/user-search.html
1001
Mike Frysinger13f23a42013-05-13 17:32:01 -04001002Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001003 $ gerrit todo # List all the CLs that await your review.
1004 $ gerrit mine # List all of your open CLs.
1005 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1006 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1007 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001008 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
100928123.
1010 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1011CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001012Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001013 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1014with Commit-Queue=1.
1015 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1016CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001017 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001018
Harry Cutts26076b32019-02-26 15:01:29 -08001019Actions:
1020"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001021 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001022
Mike Frysinger65fc8632020-02-06 18:11:12 -05001023 actions = _GetActions()
1024
Alex Klein2ab29cc2018-07-19 12:01:00 -06001025 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -04001026 parser = commandline.ArgumentParser(
1027 description=description, default_log_level='notice')
Mike Frysinger8674a112021-02-09 14:44:17 -05001028
1029 group = parser.add_argument_group('Server options')
1030 group.add_argument('-i', '--internal', dest='gob', action='store_const',
1031 default=site_params.EXTERNAL_GOB_INSTANCE,
1032 const=site_params.INTERNAL_GOB_INSTANCE,
1033 help='Query internal Chrome Gerrit instance')
1034 group.add_argument('-g', '--gob',
1035 default=site_params.EXTERNAL_GOB_INSTANCE,
1036 help='Gerrit (on borg) instance to query (default: %s)' %
1037 (site_params.EXTERNAL_GOB_INSTANCE))
1038
1039 def _AddCommonOptions(p):
1040 """Add options that should work before & after the subcommand.
1041
1042 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1043 """
1044 parser.add_common_argument_to_group(
1045 p, '--ne', '--no-emails', dest='notify',
1046 default='ALL', action='store_const', const='NONE',
1047 help='Do not send e-mail notifications')
1048 parser.add_common_argument_to_group(
1049 p, '-n', '--dry-run', dest='dryrun',
1050 default=False, action='store_true',
1051 help='Show what would be done, but do not make changes')
1052
1053 group = parser.add_argument_group('CL options')
1054 _AddCommonOptions(group)
1055
Mike Frysingerf70bdc72014-06-15 00:44:06 -07001056 parser.add_argument('--raw', default=False, action='store_true',
1057 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -04001058 parser.add_argument('--json', default=False, action='store_true',
1059 help='Return results in JSON (suitable for scripting)')
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001060
1061 # Subparsers are required by default under Python 2. Python 3 changed to
1062 # not required, but didn't include a required option until 3.7. Setting
1063 # the required member works in all versions (and setting dest name).
1064 subparsers = parser.add_subparsers(dest='action')
1065 subparsers.required = True
1066 for cmd, cls in actions.items():
1067 # Format the full docstring by removing the file level indentation.
1068 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
1069 subparser = subparsers.add_parser(cmd, description=description)
Mike Frysinger8674a112021-02-09 14:44:17 -05001070 _AddCommonOptions(subparser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001071 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001072
1073 return parser
1074
1075
1076def main(argv):
1077 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -05001078 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001079
Mike Frysinger484e2f82020-03-20 01:41:10 -04001080 # In case the action wants to throw a parser error.
1081 opts.parser = parser
1082
Mike Frysinger88f27292014-06-17 09:40:45 -07001083 # A cache of gerrit helpers we'll load on demand.
1084 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001085
Mike Frysinger88f27292014-06-17 09:40:45 -07001086 opts.Freeze()
1087
Mike Frysinger27e21b72018-07-12 14:20:21 -04001088 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001089 global COLOR
1090 COLOR = terminal.Color(enabled=opts.color)
1091
Mike Frysinger13f23a42013-05-13 17:32:01 -04001092 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001093 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001094 obj = actions[opts.action]()
Mike Frysinger65fc8632020-02-06 18:11:12 -05001095 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001096 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001097 except (cros_build_lib.RunCommandError, gerrit.GerritException,
1098 gob_util.GOBError) as e:
1099 cros_build_lib.Die(e)