blob: 6e65ceb11adf652edbd274a5b27bdacbc31e8958 [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 Frysinger7f2018d2021-02-04 00:10:58 -050030from chromite.lib import pformat
Mike Frysinger13f23a42013-05-13 17:32:01 -040031from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040032from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060033from chromite.utils import memoize
Mike Frysinger13f23a42013-05-13 17:32:01 -040034
35
Mike Frysinger1c76d4c2020-02-08 23:35:29 -050036assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
37
38
Mike Frysingerc7796cf2020-02-06 23:55:15 -050039class UserAction(object):
40 """Base class for all custom user actions."""
41
42 # The name of the command the user types in.
43 COMMAND = None
44
45 @staticmethod
46 def init_subparser(parser):
47 """Add arguments to this action's subparser."""
48
49 @staticmethod
50 def __call__(opts):
51 """Implement the action."""
Mike Frysinger62178ae2020-03-20 01:37:43 -040052 raise RuntimeError('Internal error: action missing __call__ implementation')
Mike Frysinger108eda22018-06-06 18:45:12 -040053
54
Mike Frysinger254f33f2019-12-11 13:54:29 -050055# How many connections we'll use in parallel. We don't want this to be too high
56# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
57# seems to be good enough for users.
58CONNECTION_LIMIT = 10
59
60
Mike Frysinger031ad0b2013-05-14 18:15:34 -040061COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040062
63# Map the internal names to the ones we normally show on the web ui.
64GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080065 'COMR': ['CQ', 'Commit Queue ',],
66 'CRVW': ['CR', 'Code Review ',],
67 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080068 'VRIF': ['V ', 'Verified ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060069 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040070}
71
72# Order is important -- matches the web ui. This also controls the short
73# entries that we summarize in non-verbose mode.
74GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
75
Mike Frysinger4aea5dc2019-07-17 13:39:56 -040076# Shorter strings for CL status messages.
77GERRIT_SUMMARY_MAP = {
78 'ABANDONED': 'ABD',
79 'MERGED': 'MRG',
80 'NEW': 'NEW',
81 'WIP': 'WIP',
82}
83
Mike Frysinger13f23a42013-05-13 17:32:01 -040084
85def red(s):
86 return COLOR.Color(terminal.Color.RED, s)
87
88
89def green(s):
90 return COLOR.Color(terminal.Color.GREEN, s)
91
92
93def blue(s):
94 return COLOR.Color(terminal.Color.BLUE, s)
95
96
Mike Frysinger254f33f2019-12-11 13:54:29 -050097def _run_parallel_tasks(task, *args):
98 """Small wrapper around BackgroundTaskRunner to enforce job count."""
99 with parallel.BackgroundTaskRunner(task, processes=CONNECTION_LIMIT) as q:
100 for arg in args:
101 q.put([arg])
102
103
Mike Frysinger13f23a42013-05-13 17:32:01 -0400104def limits(cls):
105 """Given a dict of fields, calculate the longest string lengths
106
107 This allows you to easily format the output of many results so that the
108 various cols all line up correctly.
109 """
110 lims = {}
111 for cl in cls:
112 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -0400113 # Use %s rather than str() to avoid codec issues.
114 # We also do this so we can format integers.
115 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400116 return lims
117
118
Mike Frysinger88f27292014-06-17 09:40:45 -0700119# TODO: This func really needs to be merged into the core gerrit logic.
120def GetGerrit(opts, cl=None):
121 """Auto pick the right gerrit instance based on the |cl|
122
123 Args:
124 opts: The general options object.
125 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
126
127 Returns:
128 A tuple of a gerrit object and a sanitized CL #.
129 """
130 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700131 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600132 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600133 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600134 if cl.startswith('*'):
135 cl = cl[1:]
136 else:
137 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700138 elif ':' in cl:
139 gob, cl = cl.split(':', 1)
140
141 if not gob in opts.gerrit:
142 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
143
144 return (opts.gerrit[gob], cl)
145
146
Mike Frysinger13f23a42013-05-13 17:32:01 -0400147def GetApprovalSummary(_opts, cls):
148 """Return a dict of the most important approvals"""
149 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700150 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
151 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
152 if not cats:
153 logging.warning('unknown gerrit approval type: %s', approver['type'])
154 continue
155 cat = cats[0].strip()
156 val = int(approver['value'])
157 if not cat in approvs:
158 # Ignore the extended categories in the summary view.
159 continue
160 elif approvs[cat] == '':
161 approvs[cat] = val
162 elif val < 0:
163 approvs[cat] = min(approvs[cat], val)
164 else:
165 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400166 return approvs
167
168
Mike Frysingera1b4b272017-04-05 16:11:00 -0400169def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400170 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400171 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400172 lims = {'url': 0, 'project': 0}
173
174 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400175
176 if opts.verbose:
177 status += '%s ' % (cl['status'],)
178 else:
179 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
180
Mike Frysinger13f23a42013-05-13 17:32:01 -0400181 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400182 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400183 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400184 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400185 functor = lambda x: x
186 elif approvs[cat] < 0:
187 functor = red
188 else:
189 functor = green
190 status += functor('%s:%2s ' % (cat, approvs[cat]))
191
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400192 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
193 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400194
195 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400196 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400197 functor = red if int(approver['value']) < 0 else green
198 n = functor('%2s' % approver['value'])
199 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
200 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500201 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400202
203
Mike Frysingera1b4b272017-04-05 16:11:00 -0400204def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400205 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400206 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600207 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400208 pfx = ''
209 # Special case internal Chrome GoB as that is what most devs use.
210 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600211 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
212 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400213 for cl in cls:
214 print('%s%s' % (pfx, cl['number']))
215
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400216 elif opts.json:
217 json.dump(cls, sys.stdout)
218
Mike Frysingera1b4b272017-04-05 16:11:00 -0400219 else:
220 if lims is None:
221 lims = limits(cls)
222
223 for cl in cls:
224 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
225
226
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400227def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700228 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800229 if opts.branch is not None:
230 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800231 if opts.project is not None:
232 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800233 if opts.topic is not None:
234 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800235
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400236 if helper is None:
237 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700238 return helper.Query(query, raw=raw, bypass_cache=False)
239
240
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400241def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700242 """Query gerrit and filter/clean up the results"""
243 ret = []
244
Mike Frysinger2cd56022017-01-12 20:56:27 -0500245 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400246 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400247 # Gerrit likes to return a stats record too.
248 if not 'project' in cl:
249 continue
250
251 # Strip off common leading names since the result is still
252 # unique over the whole tree.
253 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400254 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
255 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400256 if cl['project'].startswith('%s/' % pfx):
257 cl['project'] = cl['project'][len(pfx) + 1:]
258
Mike Frysinger479f1192017-09-14 22:36:30 -0400259 cl['url'] = uri_lib.ShortenUri(cl['url'])
260
Mike Frysinger13f23a42013-05-13 17:32:01 -0400261 ret.append(cl)
262
Mike Frysingerb62313a2017-06-30 16:38:58 -0400263 if opts.sort == 'unsorted':
264 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700265 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400266 key = lambda x: int(x[opts.sort])
267 else:
268 key = lambda x: x[opts.sort]
269 return sorted(ret, key=key)
270
271
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500272class _ActionSearchQuery(UserAction):
273 """Base class for actions that perform searches."""
274
275 @staticmethod
276 def init_subparser(parser):
277 """Add arguments to this action's subparser."""
278 parser.add_argument('--sort', default='number',
279 help='Key to sort on (number, project); use "unsorted" '
280 'to disable')
281 parser.add_argument('-b', '--branch',
282 help='Limit output to the specific branch')
283 parser.add_argument('-p', '--project',
284 help='Limit output to the specific project')
285 parser.add_argument('-t', '--topic',
286 help='Limit output to the specific topic')
287
288
289class ActionTodo(_ActionSearchQuery):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400290 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500291
292 COMMAND = 'todo'
293
294 @staticmethod
295 def __call__(opts):
296 """Implement the action."""
297 cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
298 'label:Code-Review=0,user=self '
299 'NOT label:Verified<0'))
300 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400301
302
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500303class ActionSearch(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800304 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500305
306 COMMAND = 'search'
307
308 @staticmethod
309 def init_subparser(parser):
310 """Add arguments to this action's subparser."""
311 _ActionSearchQuery.init_subparser(parser)
312 parser.add_argument('query',
313 help='The search query')
314
315 @staticmethod
316 def __call__(opts):
317 """Implement the action."""
318 cls = FilteredQuery(opts, opts.query)
319 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400320
321
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500322class ActionMine(_ActionSearchQuery):
Mike Frysingera1db2c42014-06-15 00:42:48 -0700323 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500324
325 COMMAND = 'mine'
326
327 @staticmethod
328 def init_subparser(parser):
329 """Add arguments to this action's subparser."""
330 _ActionSearchQuery.init_subparser(parser)
331 parser.add_argument('--draft', default=False, action='store_true',
332 help='Show draft changes')
333
334 @staticmethod
335 def __call__(opts):
336 """Implement the action."""
337 if opts.draft:
338 rule = 'is:draft'
339 else:
340 rule = 'status:new'
341 cls = FilteredQuery(opts, 'owner:self %s' % (rule,))
342 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700343
344
Paul Hobbs89765232015-06-24 14:07:49 -0700345def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
346 """Runs breadth first search starting from the nodes in |to_visit|
347
348 Args:
349 to_visit: the starting nodes
350 children: a function which takes a node and returns the nodes adjacent to it
351 visited_key: a function for deduplicating node visits. Defaults to the
352 identity function (lambda x: x)
353
354 Returns:
355 A list of nodes which are reachable from any node in |to_visit| by calling
356 |children| any number of times.
357 """
358 to_visit = list(to_visit)
Mike Frysinger66ce4132019-07-17 22:52:52 -0400359 seen = set(visited_key(x) for x in to_visit)
Paul Hobbs89765232015-06-24 14:07:49 -0700360 for node in to_visit:
361 for child in children(node):
362 key = visited_key(child)
363 if key not in seen:
364 seen.add(key)
365 to_visit.append(child)
366 return to_visit
367
368
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500369class ActionDeps(_ActionSearchQuery):
Paul Hobbs89765232015-06-24 14:07:49 -0700370 """List CLs matching a query, and all transitive dependencies of those CLs"""
Paul Hobbs89765232015-06-24 14:07:49 -0700371
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500372 COMMAND = 'deps'
Paul Hobbs89765232015-06-24 14:07:49 -0700373
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500374 @staticmethod
375 def init_subparser(parser):
376 """Add arguments to this action's subparser."""
377 _ActionSearchQuery.init_subparser(parser)
378 parser.add_argument('query',
379 help='The search query')
380
381 def __call__(self, opts):
382 """Implement the action."""
383 cls = _Query(opts, opts.query, raw=False)
384
385 @memoize.Memoize
386 def _QueryChange(cl, helper=None):
387 return _Query(opts, cl, raw=False, helper=helper)
388
389 transitives = _BreadthFirstSearch(
390 cls, functools.partial(self._Children, opts, _QueryChange),
Mike Frysingerdc407f52020-05-08 00:34:56 -0400391 visited_key=lambda cl: cl.PatchLink())
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500392
Mike Frysingerdc407f52020-05-08 00:34:56 -0400393 # This is a hack to avoid losing GoB host for each CL. The PrintCls
394 # function assumes the GoB host specified by the user is the only one
395 # that is ever used, but the deps command walks across hosts.
396 if opts.raw:
397 print('\n'.join(x.PatchLink() for x in transitives))
398 else:
399 transitives_raw = [cl.patch_dict for cl in transitives]
400 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500401
402 @staticmethod
403 def _ProcessDeps(opts, querier, cl, deps, required):
Mike Frysinger5726da92017-09-20 22:14:25 -0400404 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700405 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400406 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400407 if not dep.remote in opts.gerrit:
408 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
409 remote=dep.remote, print_cmd=opts.debug)
410 helper = opts.gerrit[dep.remote]
411
Paul Hobbs89765232015-06-24 14:07:49 -0700412 # TODO(phobbs) this should maybe catch network errors.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500413 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400414
415 # Handle empty results. If we found a commit that was pushed directly
416 # (e.g. a bot commit), then gerrit won't know about it.
417 if not changes:
418 if required:
419 logging.error('CL %s depends on %s which cannot be found',
420 cl, dep.ToGerritQueryText())
421 continue
422
423 # Our query might have matched more than one result. This can come up
424 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
425 # across multiple repos/branches. We blindly check all of them in the
426 # hopes that all open ones are what the user wants, but then again the
427 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
428 if len(changes) > 1:
429 logging.warning('CL %s has an ambiguous CQ dependency %s',
430 cl, dep.ToGerritQueryText())
431 for change in changes:
432 if change.status == 'NEW':
433 yield change
434
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500435 @classmethod
436 def _Children(cls, opts, querier, cl):
Mike Frysinger5726da92017-09-20 22:14:25 -0400437 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500438 for change in cls._ProcessDeps(
439 opts, querier, cl, cl.PaladinDependencies(None), True):
Mike Frysinger5726da92017-09-20 22:14:25 -0400440 yield change
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500441 for change in cls._ProcessDeps(
442 opts, querier, cl, cl.GerritDependencies(), False):
Mike Frysinger5726da92017-09-20 22:14:25 -0400443 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700444
Paul Hobbs89765232015-06-24 14:07:49 -0700445
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500446class ActionInspect(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800447 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500448
449 COMMAND = 'inspect'
450
451 @staticmethod
452 def init_subparser(parser):
453 """Add arguments to this action's subparser."""
454 _ActionSearchQuery.init_subparser(parser)
455 parser.add_argument('cls', nargs='+', metavar='CL',
456 help='The CL(s) to update')
457
458 @staticmethod
459 def __call__(opts):
460 """Implement the action."""
461 cls = []
462 for arg in opts.cls:
463 helper, cl = GetGerrit(opts, arg)
464 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
465 if change:
466 cls.extend(change)
467 else:
468 logging.warning('no results found for CL %s', arg)
469 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400470
471
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500472class _ActionLabeler(UserAction):
473 """Base helper for setting labels."""
474
475 LABEL = None
476 VALUES = None
477
478 @classmethod
479 def init_subparser(cls, parser):
480 """Add arguments to this action's subparser."""
481 parser.add_argument('--ne', '--no-emails', dest='notify',
482 default='ALL', action='store_const', const='NONE',
483 help='Do not send e-mail notifications')
484 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
485 help='Optional message to include')
486 parser.add_argument('cls', nargs='+', metavar='CL',
487 help='The CL(s) to update')
488 parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
489 help='The label value; one of [%(choices)s]')
490
491 @classmethod
492 def __call__(cls, opts):
493 """Implement the action."""
494 # Convert user friendly command line option into a gerrit parameter.
495 def task(arg):
496 helper, cl = GetGerrit(opts, arg)
497 helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
498 dryrun=opts.dryrun, notify=opts.notify)
499 _run_parallel_tasks(task, *opts.cls)
500
501
502class ActionLabelAutoSubmit(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500503 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500504
505 COMMAND = 'label-as'
506 LABEL = 'Auto-Submit'
507 VALUES = ('0', '1')
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600508
509
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500510class ActionLabelCodeReview(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500511 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500512
513 COMMAND = 'label-cr'
514 LABEL = 'Code-Review'
515 VALUES = ('-2', '-1', '0', '1', '2')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400516
517
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500518class ActionLabelVerified(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500519 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500520
521 COMMAND = 'label-v'
522 LABEL = 'Verified'
523 VALUES = ('-1', '0', '1')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400524
525
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500526class ActionLabelCommitQueue(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500527 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500528
529 COMMAND = 'label-cq'
530 LABEL = 'Commit-Queue'
531 VALUES = ('0', '1', '2')
Mike Frysinger15b23e42014-12-05 17:00:05 -0500532
533
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500534class _ActionSimpleParallelCLs(UserAction):
535 """Base helper for actions that only accept CLs."""
536
537 @staticmethod
538 def init_subparser(parser):
539 """Add arguments to this action's subparser."""
540 parser.add_argument('cls', nargs='+', metavar='CL',
541 help='The CL(s) to update')
542
543 def __call__(self, opts):
544 """Implement the action."""
545 def task(arg):
546 helper, cl = GetGerrit(opts, arg)
547 self._process_one(helper, cl, opts)
548 _run_parallel_tasks(task, *opts.cls)
549
550
551class ActionSubmit(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800552 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500553
554 COMMAND = 'submit'
555
556 @staticmethod
557 def _process_one(helper, cl, opts):
558 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700559 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400560
561
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500562class ActionAbandon(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800563 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500564
565 COMMAND = 'abandon'
566
567 @staticmethod
568 def _process_one(helper, cl, opts):
569 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700570 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400571
572
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500573class ActionRestore(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800574 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500575
576 COMMAND = 'restore'
577
578 @staticmethod
579 def _process_one(helper, cl, opts):
580 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700581 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400582
583
Tomasz Figa54d70992021-01-20 13:48:59 +0900584class ActionWorkInProgress(_ActionSimpleParallelCLs):
585 """Mark CLs as work in progress"""
586
587 COMMAND = 'wip'
588
589 @staticmethod
590 def _process_one(helper, cl, opts):
591 """Use |helper| to process the single |cl|."""
592 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
593
594
595class ActionReadyForReview(_ActionSimpleParallelCLs):
596 """Mark CLs as ready for review"""
597
598 COMMAND = 'ready'
599
600 @staticmethod
601 def _process_one(helper, cl, opts):
602 """Use |helper| to process the single |cl|."""
603 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
604
605
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500606class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800607 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700608
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500609 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700610
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500611 @staticmethod
612 def init_subparser(parser):
613 """Add arguments to this action's subparser."""
614 parser.add_argument('--ne', '--no-emails', dest='notify',
615 default='ALL', action='store_const', const='NONE',
616 help='Do not send e-mail notifications')
617 parser.add_argument('cl', metavar='CL',
618 help='The CL to update')
619 parser.add_argument('reviewers', nargs='+',
620 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700621
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500622 @staticmethod
623 def __call__(opts):
624 """Implement the action."""
625 # Allow for optional leading '~'.
626 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
627 add_list, remove_list, invalid_list = [], [], []
628
629 for email in opts.reviewers:
630 if not email_validator.match(email):
631 invalid_list.append(email)
632 elif email[0] == '~':
633 remove_list.append(email[1:])
634 else:
635 add_list.append(email)
636
637 if invalid_list:
638 cros_build_lib.Die(
639 'Invalid email address(es): %s' % ', '.join(invalid_list))
640
641 if add_list or remove_list:
642 helper, cl = GetGerrit(opts, opts.cl)
643 helper.SetReviewers(cl, add=add_list, remove=remove_list,
644 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700645
646
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500647class ActionAssign(_ActionSimpleParallelCLs):
648 """Set the assignee for CLs"""
649
650 COMMAND = 'assign'
651
652 @staticmethod
653 def init_subparser(parser):
654 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400655 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500656 parser.add_argument('assignee',
657 help='The new assignee')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500658
659 @staticmethod
660 def _process_one(helper, cl, opts):
661 """Use |helper| to process the single |cl|."""
662 helper.SetAssignee(cl, opts.assignee, dryrun=opts.dryrun)
Allen Li38abdaa2017-03-16 13:25:02 -0700663
664
Mike Frysinger62178ae2020-03-20 01:37:43 -0400665class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800666 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500667
668 COMMAND = 'message'
669
670 @staticmethod
671 def init_subparser(parser):
672 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400673 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500674 parser.add_argument('message',
675 help='The message to post')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500676
677 @staticmethod
678 def _process_one(helper, cl, opts):
679 """Use |helper| to process the single |cl|."""
680 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530681
682
Mike Frysinger62178ae2020-03-20 01:37:43 -0400683class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800684 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500685
686 COMMAND = 'topic'
687
688 @staticmethod
689 def init_subparser(parser):
690 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400691 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500692 parser.add_argument('topic',
693 help='The topic to set')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500694
695 @staticmethod
696 def _process_one(helper, cl, opts):
697 """Use |helper| to process the single |cl|."""
698 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800699
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800700
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500701class ActionPrivate(_ActionSimpleParallelCLs):
702 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700703
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500704 COMMAND = 'private'
705
706 @staticmethod
707 def _process_one(helper, cl, opts):
708 """Use |helper| to process the single |cl|."""
709 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700710
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800711
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500712class ActionPublic(_ActionSimpleParallelCLs):
713 """Mark CLs public"""
714
715 COMMAND = 'public'
716
717 @staticmethod
718 def _process_one(helper, cl, opts):
719 """Use |helper| to process the single |cl|."""
720 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
721
722
723class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800724 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500725
726 COMMAND = 'hashtags'
727
728 @staticmethod
729 def init_subparser(parser):
730 """Add arguments to this action's subparser."""
731 parser.add_argument('cl', metavar='CL',
732 help='The CL to update')
733 parser.add_argument('hashtags', nargs='+',
734 help='The hashtags to add/remove')
735
736 @staticmethod
737 def __call__(opts):
738 """Implement the action."""
739 add = []
740 remove = []
741 for hashtag in opts.hashtags:
742 if hashtag.startswith('~'):
743 remove.append(hashtag[1:])
744 else:
745 add.append(hashtag)
746 helper, cl = GetGerrit(opts, opts.cl)
747 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800748
749
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500750class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800751 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500752
753 COMMAND = 'deletedraft'
754
755 @staticmethod
756 def _process_one(helper, cl, opts):
757 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700758 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800759
760
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500761class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500762 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500763
764 COMMAND = 'reviewed'
765
766 @staticmethod
767 def _process_one(helper, cl, opts):
768 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500769 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500770
771
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500772class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500773 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500774
775 COMMAND = 'unreviewed'
776
777 @staticmethod
778 def _process_one(helper, cl, opts):
779 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500780 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500781
782
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500783class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500784 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500785
786 COMMAND = 'ignore'
787
788 @staticmethod
789 def _process_one(helper, cl, opts):
790 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500791 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500792
793
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500794class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500795 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500796
797 COMMAND = 'unignore'
798
799 @staticmethod
800 def _process_one(helper, cl, opts):
801 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500802 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500803
804
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400805class ActionCherryPick(UserAction):
806 """Cherry pick CLs to branches."""
807
808 COMMAND = 'cherry-pick'
809
810 @staticmethod
811 def init_subparser(parser):
812 """Add arguments to this action's subparser."""
813 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
814 parser.add_argument('--rev', '--revision', default='current',
815 help='A specific revision or patchset')
816 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
817 help='Include a message')
818 parser.add_argument('--branches', '--branch', '--br', action='split_extend',
819 default=[], required=True,
820 help='The destination branches')
821 parser.add_argument('cls', nargs='+', metavar='CL',
822 help='The CLs to cherry-pick')
823
824 @staticmethod
825 def __call__(opts):
826 """Implement the action."""
827 # Process branches in parallel, but CLs in serial in case of CL stacks.
828 def task(branch):
829 for arg in opts.cls:
830 helper, cl = GetGerrit(opts, arg)
831 ret = helper.CherryPick(cl, branch, rev=opts.rev, msg=opts.msg,
832 dryrun=opts.dryrun)
833 logging.debug('Response: %s', ret)
834 if opts.raw:
835 print(ret['_number'])
836 else:
837 uri = f'https://{helper.host}/c/{ret["_number"]}'
838 print(uri_lib.ShortenUri(uri))
839
840 _run_parallel_tasks(task, *opts.branches)
841
842
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500843class ActionAccount(_ActionSimpleParallelCLs):
844 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500845
846 COMMAND = 'account'
847
848 @staticmethod
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500849 def init_subparser(parser):
850 """Add arguments to this action's subparser."""
851 parser.add_argument('accounts', nargs='*', default=['self'],
852 help='The accounts to query')
853
854 @classmethod
855 def __call__(cls, opts):
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500856 """Implement the action."""
857 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -0500858
859 def print_one(header, data):
860 print(f'### {header}')
861 print(pformat.json(data, compact=opts.json).rstrip())
862
863 def task(arg):
864 detail = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/detail')
865 if not detail:
866 print(f'{arg}: account not found')
867 else:
868 print_one('detail', detail)
869 for field in ('groups', 'capabilities', 'preferences', 'sshkeys',
870 'gpgkeys'):
871 data = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/{field}')
872 print_one(field, data)
873
874 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800875
876
Mike Frysinger484e2f82020-03-20 01:41:10 -0400877class ActionHelpAll(UserAction):
878 """Show all actions help output at once."""
879
880 COMMAND = 'help-all'
881
882 @staticmethod
883 def __call__(opts):
884 """Implement the action."""
885 first = True
886 for action in _GetActions():
887 if first:
888 first = False
889 else:
890 print('\n\n')
891
892 try:
893 opts.parser.parse_args([action, '--help'])
894 except SystemExit:
895 pass
896
897
Mike Frysinger65fc8632020-02-06 18:11:12 -0500898@memoize.Memoize
899def _GetActions():
900 """Get all the possible actions we support.
901
902 Returns:
903 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
904 function that implements that command (e.g. UserActFoo).
905 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500906 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
907
908 actions = {}
909 for cls in globals().values():
910 if (not inspect.isclass(cls) or
911 not issubclass(cls, UserAction) or
912 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -0500913 continue
914
Mike Frysinger65fc8632020-02-06 18:11:12 -0500915 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500916 cmd = cls.COMMAND
917 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
918 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -0500919
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500920 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -0500921
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500922 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500923
924
Harry Cutts26076b32019-02-26 15:01:29 -0800925def _GetActionUsages():
926 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -0500927 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -0800928
Mike Frysinger65fc8632020-02-06 18:11:12 -0500929 cmds = list(actions.keys())
930 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -0800931 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500932 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -0800933
Harry Cutts26076b32019-02-26 15:01:29 -0800934 cmd_indent = len(max(cmds, key=len))
935 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500936 return '\n'.join(
937 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
938 for cmd, usage, doc in zip(cmds, usages, docs)
939 )
Harry Cutts26076b32019-02-26 15:01:29 -0800940
941
Mike Frysinger108eda22018-06-06 18:45:12 -0400942def GetParser():
943 """Returns the parser to use for this module."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500944 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -0400945There is no support for doing line-by-line code review via the command line.
946This helps you manage various bits and CL status.
947
Mike Frysingera1db2c42014-06-15 00:42:48 -0700948For general Gerrit documentation, see:
949 https://gerrit-review.googlesource.com/Documentation/
950The Searching Changes page covers the search query syntax:
951 https://gerrit-review.googlesource.com/Documentation/user-search.html
952
Mike Frysinger13f23a42013-05-13 17:32:01 -0400953Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500954 $ gerrit todo # List all the CLs that await your review.
955 $ gerrit mine # List all of your open CLs.
956 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
957 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
958 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800959 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
96028123.
961 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
962CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700963Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500964 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
965with Commit-Queue=1.
966 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
967CLs with Commit-Queue=1.
968 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400969
Harry Cutts26076b32019-02-26 15:01:29 -0800970Actions:
971"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500972 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400973
Mike Frysinger65fc8632020-02-06 18:11:12 -0500974 actions = _GetActions()
975
Alex Klein2ab29cc2018-07-19 12:01:00 -0600976 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -0400977 parser = commandline.ArgumentParser(
978 description=description, default_log_level='notice')
Mike Frysinger08737512014-02-07 22:58:26 -0500979 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600980 default=site_params.EXTERNAL_GOB_INSTANCE,
981 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500982 help='Query internal Chromium Gerrit instance')
983 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600984 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500985 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600986 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700987 parser.add_argument('--raw', default=False, action='store_true',
988 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400989 parser.add_argument('--json', default=False, action='store_true',
990 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700991 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
992 dest='dryrun',
993 help='Show what would be done, but do not make changes')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500994
995 # Subparsers are required by default under Python 2. Python 3 changed to
996 # not required, but didn't include a required option until 3.7. Setting
997 # the required member works in all versions (and setting dest name).
998 subparsers = parser.add_subparsers(dest='action')
999 subparsers.required = True
1000 for cmd, cls in actions.items():
1001 # Format the full docstring by removing the file level indentation.
1002 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
1003 subparser = subparsers.add_parser(cmd, description=description)
1004 subparser.add_argument('-n', '--dry-run', dest='dryrun',
1005 default=False, action='store_true',
1006 help='Show what would be done only')
1007 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001008
1009 return parser
1010
1011
1012def main(argv):
1013 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -05001014 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001015
Mike Frysinger484e2f82020-03-20 01:41:10 -04001016 # In case the action wants to throw a parser error.
1017 opts.parser = parser
1018
Mike Frysinger88f27292014-06-17 09:40:45 -07001019 # A cache of gerrit helpers we'll load on demand.
1020 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001021
Mike Frysinger88f27292014-06-17 09:40:45 -07001022 opts.Freeze()
1023
Mike Frysinger27e21b72018-07-12 14:20:21 -04001024 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001025 global COLOR
1026 COLOR = terminal.Color(enabled=opts.color)
1027
Mike Frysinger13f23a42013-05-13 17:32:01 -04001028 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001029 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001030 obj = actions[opts.action]()
Mike Frysinger65fc8632020-02-06 18:11:12 -05001031 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001032 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001033 except (cros_build_lib.RunCommandError, gerrit.GerritException,
1034 gob_util.GOBError) as e:
1035 cros_build_lib.Die(e)