blob: 29b994fafca6256bd79a0af1bd50e47cef6e7a17 [file] [log] [blame]
Mike Frysinger13f23a42013-05-13 17:32:01 -04001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Mike Frysinger08737512014-02-07 22:58:26 -05005"""A command line interface to Gerrit-on-borg instances.
Mike Frysinger13f23a42013-05-13 17:32:01 -04006
7Internal Note:
8To expose a function directly to the command line interface, name your function
9with the prefix "UserAct".
10"""
11
Mike Frysinger8037f752020-02-29 20:47:09 -050012import argparse
Mike Frysinger65fc8632020-02-06 18:11:12 -050013import collections
Mike Frysinger2295d792021-03-08 15:55:23 -050014import configparser
Jack Rosenthale3a92672022-06-29 14:54:48 -060015import enum
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
Chris McDonald59650c32021-07-20 15:29:28 -060019import logging
Jack Rosenthal95aac172022-06-30 15:35:07 -060020import os
Mike Frysinger2295d792021-03-08 15:55:23 -050021from pathlib import Path
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070022import re
Mike Frysinger2295d792021-03-08 15:55:23 -050023import shlex
Jack Rosenthal95aac172022-06-30 15:35:07 -060024import signal
25import subprocess
Mike Frysinger87c74ce2017-04-04 16:12:31 -040026import sys
Mike Frysinger13f23a42013-05-13 17:32:01 -040027
Mike Frysinger2295d792021-03-08 15:55:23 -050028from chromite.lib import chromite_config
Chris McDonald59650c32021-07-20 15:29:28 -060029from chromite.lib import commandline
Aviv Keshetb7519e12016-10-04 00:50:00 -070030from chromite.lib import config_lib
31from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040032from chromite.lib import cros_build_lib
33from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050034from chromite.lib import gob_util
Mike Frysinger254f33f2019-12-11 13:54:29 -050035from chromite.lib import parallel
Mike Frysingera9751c92021-04-30 10:12:37 -040036from chromite.lib import retry_util
Mike Frysinger13f23a42013-05-13 17:32:01 -040037from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040038from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060039from chromite.utils import memoize
Alex Klein73eba212021-09-09 11:43:33 -060040from chromite.utils import pformat
Mike Frysinger13f23a42013-05-13 17:32:01 -040041
42
Mike Frysinger2295d792021-03-08 15:55:23 -050043class Config:
44 """Manage the user's gerrit config settings.
45
46 This is entirely unique to this gerrit command. Inspiration for naming and
47 layout is taken from ~/.gitconfig settings.
48 """
49
50 def __init__(self, path: Path = chromite_config.GERRIT_CONFIG):
51 self.cfg = configparser.ConfigParser(interpolation=None)
52 if path.exists():
53 self.cfg.read(chromite_config.GERRIT_CONFIG)
54
55 def expand_alias(self, action):
56 """Expand any aliases."""
57 alias = self.cfg.get('alias', action, fallback=None)
58 if alias is not None:
59 return shlex.split(alias)
60 return action
61
62
Mike Frysingerc7796cf2020-02-06 23:55:15 -050063class UserAction(object):
64 """Base class for all custom user actions."""
65
66 # The name of the command the user types in.
67 COMMAND = None
68
Jack Rosenthal95aac172022-06-30 15:35:07 -060069 # Should output be paged?
70 USE_PAGER = False
71
Mike Frysingerc7796cf2020-02-06 23:55:15 -050072 @staticmethod
73 def init_subparser(parser):
74 """Add arguments to this action's subparser."""
75
76 @staticmethod
77 def __call__(opts):
78 """Implement the action."""
Mike Frysinger62178ae2020-03-20 01:37:43 -040079 raise RuntimeError('Internal error: action missing __call__ implementation')
Mike Frysinger108eda22018-06-06 18:45:12 -040080
81
Mike Frysinger254f33f2019-12-11 13:54:29 -050082# How many connections we'll use in parallel. We don't want this to be too high
83# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
84# seems to be good enough for users.
85CONNECTION_LIMIT = 10
86
87
Mike Frysinger031ad0b2013-05-14 18:15:34 -040088COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040089
90# Map the internal names to the ones we normally show on the web ui.
91GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080092 'COMR': ['CQ', 'Commit Queue ',],
93 'CRVW': ['CR', 'Code Review ',],
94 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080095 'VRIF': ['V ', 'Verified ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060096 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040097}
98
99# Order is important -- matches the web ui. This also controls the short
100# entries that we summarize in non-verbose mode.
101GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
102
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400103# Shorter strings for CL status messages.
104GERRIT_SUMMARY_MAP = {
105 'ABANDONED': 'ABD',
106 'MERGED': 'MRG',
107 'NEW': 'NEW',
108 'WIP': 'WIP',
109}
110
Mike Frysinger13f23a42013-05-13 17:32:01 -0400111
Jack Rosenthale3a92672022-06-29 14:54:48 -0600112class OutputFormat(enum.Enum):
113 """Type for the requested output format.
114
115 AUTO: Automatically determine the format based on what the user
Jack Rosenthal6f4ed452022-06-30 20:07:02 -0600116 might want. This is PRETTY if attached to a terminal, RAW
117 otherwise.
Jack Rosenthale3a92672022-06-29 14:54:48 -0600118 RAW: Output CLs one per line, suitable for mild scripting.
119 JSON: JSON-encoded output, suitable for spicy scripting.
120 MARKDOWN: Suitable for posting in a bug or CL comment.
121 PRETTY: Suitable for viewing in a color terminal.
122 """
123 AUTO = 0
124 AUTOMATIC = AUTO
125 RAW = 1
126 JSON = 2
127 MARKDOWN = 3
128 PRETTY = 4
129
130
Mike Frysinger13f23a42013-05-13 17:32:01 -0400131def red(s):
132 return COLOR.Color(terminal.Color.RED, s)
133
134
135def green(s):
136 return COLOR.Color(terminal.Color.GREEN, s)
137
138
139def blue(s):
140 return COLOR.Color(terminal.Color.BLUE, s)
141
142
Mike Frysinger254f33f2019-12-11 13:54:29 -0500143def _run_parallel_tasks(task, *args):
144 """Small wrapper around BackgroundTaskRunner to enforce job count."""
Mike Frysingera9751c92021-04-30 10:12:37 -0400145 # When we run in parallel, we can hit the max requests limit.
146 def check_exc(e):
147 if not isinstance(e, gob_util.GOBError):
148 raise e
149 return e.http_status == 429
150
151 @retry_util.WithRetry(5, handler=check_exc, sleep=1, backoff_factor=2)
152 def retry(*args):
153 try:
154 task(*args)
155 except gob_util.GOBError as e:
156 if e.http_status != 429:
157 logging.warning('%s: skipping due: %s', args, e)
158 else:
159 raise
160
161 with parallel.BackgroundTaskRunner(retry, processes=CONNECTION_LIMIT) as q:
Mike Frysinger254f33f2019-12-11 13:54:29 -0500162 for arg in args:
163 q.put([arg])
164
165
Mike Frysinger13f23a42013-05-13 17:32:01 -0400166def limits(cls):
167 """Given a dict of fields, calculate the longest string lengths
168
169 This allows you to easily format the output of many results so that the
170 various cols all line up correctly.
171 """
172 lims = {}
173 for cl in cls:
174 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -0400175 # Use %s rather than str() to avoid codec issues.
176 # We also do this so we can format integers.
177 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400178 return lims
179
180
Mike Frysinger88f27292014-06-17 09:40:45 -0700181# TODO: This func really needs to be merged into the core gerrit logic.
182def GetGerrit(opts, cl=None):
183 """Auto pick the right gerrit instance based on the |cl|
184
185 Args:
186 opts: The general options object.
187 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
188
189 Returns:
190 A tuple of a gerrit object and a sanitized CL #.
191 """
192 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700193 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600194 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600195 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600196 if cl.startswith('*'):
197 cl = cl[1:]
198 else:
199 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700200 elif ':' in cl:
201 gob, cl = cl.split(':', 1)
202
203 if not gob in opts.gerrit:
204 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
205
206 return (opts.gerrit[gob], cl)
207
208
Mike Frysinger13f23a42013-05-13 17:32:01 -0400209def GetApprovalSummary(_opts, cls):
210 """Return a dict of the most important approvals"""
211 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700212 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
213 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
214 if not cats:
215 logging.warning('unknown gerrit approval type: %s', approver['type'])
216 continue
217 cat = cats[0].strip()
218 val = int(approver['value'])
219 if not cat in approvs:
220 # Ignore the extended categories in the summary view.
221 continue
222 elif approvs[cat] == '':
223 approvs[cat] = val
224 elif val < 0:
225 approvs[cat] = min(approvs[cat], val)
226 else:
227 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400228 return approvs
229
230
Mike Frysingera1b4b272017-04-05 16:11:00 -0400231def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400232 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400233 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400234 lims = {'url': 0, 'project': 0}
235
236 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400237
238 if opts.verbose:
239 status += '%s ' % (cl['status'],)
240 else:
241 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
242
Mike Frysinger13f23a42013-05-13 17:32:01 -0400243 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400244 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400245 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400246 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400247 functor = lambda x: x
248 elif approvs[cat] < 0:
249 functor = red
250 else:
251 functor = green
252 status += functor('%s:%2s ' % (cat, approvs[cat]))
253
Jack Rosenthale3a92672022-06-29 14:54:48 -0600254 if opts.format is OutputFormat.MARKDOWN:
Douglas Andersoncf9e9632022-05-24 14:55:16 -0700255 print('* %s - %s' % (uri_lib.ShortenUri(cl['url']), cl['subject']))
256 else:
257 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
258 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400259
260 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400261 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400262 functor = red if int(approver['value']) < 0 else green
263 n = functor('%2s' % approver['value'])
264 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
265 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500266 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400267
268
Mike Frysingera1b4b272017-04-05 16:11:00 -0400269def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400270 """Print all results based on the requested format."""
Jack Rosenthale3a92672022-06-29 14:54:48 -0600271 if opts.format is OutputFormat.RAW:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600272 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400273 pfx = ''
274 # Special case internal Chrome GoB as that is what most devs use.
275 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600276 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
277 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400278 for cl in cls:
279 print('%s%s' % (pfx, cl['number']))
280
Jack Rosenthale3a92672022-06-29 14:54:48 -0600281 elif opts.format is OutputFormat.JSON:
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400282 json.dump(cls, sys.stdout)
283
Mike Frysingera1b4b272017-04-05 16:11:00 -0400284 else:
285 if lims is None:
286 lims = limits(cls)
287
288 for cl in cls:
289 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
290
291
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400292def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700293 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800294 if opts.branch is not None:
295 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800296 if opts.project is not None:
297 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800298 if opts.topic is not None:
299 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800300
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400301 if helper is None:
302 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700303 return helper.Query(query, raw=raw, bypass_cache=False)
304
305
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400306def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700307 """Query gerrit and filter/clean up the results"""
308 ret = []
309
Mike Frysinger2cd56022017-01-12 20:56:27 -0500310 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400311 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400312 # Gerrit likes to return a stats record too.
313 if not 'project' in cl:
314 continue
315
316 # Strip off common leading names since the result is still
317 # unique over the whole tree.
318 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400319 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
320 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400321 if cl['project'].startswith('%s/' % pfx):
322 cl['project'] = cl['project'][len(pfx) + 1:]
323
Mike Frysinger479f1192017-09-14 22:36:30 -0400324 cl['url'] = uri_lib.ShortenUri(cl['url'])
325
Mike Frysinger13f23a42013-05-13 17:32:01 -0400326 ret.append(cl)
327
Mike Frysingerb62313a2017-06-30 16:38:58 -0400328 if opts.sort == 'unsorted':
329 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700330 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400331 key = lambda x: int(x[opts.sort])
332 else:
333 key = lambda x: x[opts.sort]
334 return sorted(ret, key=key)
335
336
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500337class _ActionSearchQuery(UserAction):
338 """Base class for actions that perform searches."""
339
Jack Rosenthal95aac172022-06-30 15:35:07 -0600340 USE_PAGER = True
341
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500342 @staticmethod
343 def init_subparser(parser):
344 """Add arguments to this action's subparser."""
345 parser.add_argument('--sort', default='number',
346 help='Key to sort on (number, project); use "unsorted" '
347 'to disable')
348 parser.add_argument('-b', '--branch',
349 help='Limit output to the specific branch')
350 parser.add_argument('-p', '--project',
351 help='Limit output to the specific project')
352 parser.add_argument('-t', '--topic',
353 help='Limit output to the specific topic')
354
355
356class ActionTodo(_ActionSearchQuery):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400357 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500358
359 COMMAND = 'todo'
360
361 @staticmethod
362 def __call__(opts):
363 """Implement the action."""
Mike Frysinger242d2922021-02-09 14:31:50 -0500364 cls = FilteredQuery(opts, 'attention:self')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500365 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400366
367
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500368class ActionSearch(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800369 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500370
371 COMMAND = 'search'
372
373 @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 @staticmethod
381 def __call__(opts):
382 """Implement the action."""
383 cls = FilteredQuery(opts, opts.query)
384 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400385
386
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500387class ActionMine(_ActionSearchQuery):
Mike Frysingera1db2c42014-06-15 00:42:48 -0700388 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500389
390 COMMAND = 'mine'
391
392 @staticmethod
393 def init_subparser(parser):
394 """Add arguments to this action's subparser."""
395 _ActionSearchQuery.init_subparser(parser)
396 parser.add_argument('--draft', default=False, action='store_true',
397 help='Show draft changes')
398
399 @staticmethod
400 def __call__(opts):
401 """Implement the action."""
402 if opts.draft:
403 rule = 'is:draft'
404 else:
405 rule = 'status:new'
406 cls = FilteredQuery(opts, 'owner:self %s' % (rule,))
407 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700408
409
Paul Hobbs89765232015-06-24 14:07:49 -0700410def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
411 """Runs breadth first search starting from the nodes in |to_visit|
412
413 Args:
414 to_visit: the starting nodes
415 children: a function which takes a node and returns the nodes adjacent to it
416 visited_key: a function for deduplicating node visits. Defaults to the
417 identity function (lambda x: x)
418
419 Returns:
420 A list of nodes which are reachable from any node in |to_visit| by calling
421 |children| any number of times.
422 """
423 to_visit = list(to_visit)
Mike Frysinger66ce4132019-07-17 22:52:52 -0400424 seen = set(visited_key(x) for x in to_visit)
Paul Hobbs89765232015-06-24 14:07:49 -0700425 for node in to_visit:
426 for child in children(node):
427 key = visited_key(child)
428 if key not in seen:
429 seen.add(key)
430 to_visit.append(child)
431 return to_visit
432
433
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500434class ActionDeps(_ActionSearchQuery):
Paul Hobbs89765232015-06-24 14:07:49 -0700435 """List CLs matching a query, and all transitive dependencies of those CLs"""
Paul Hobbs89765232015-06-24 14:07:49 -0700436
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500437 COMMAND = 'deps'
Paul Hobbs89765232015-06-24 14:07:49 -0700438
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500439 @staticmethod
440 def init_subparser(parser):
441 """Add arguments to this action's subparser."""
442 _ActionSearchQuery.init_subparser(parser)
443 parser.add_argument('query',
444 help='The search query')
445
446 def __call__(self, opts):
447 """Implement the action."""
448 cls = _Query(opts, opts.query, raw=False)
449
450 @memoize.Memoize
451 def _QueryChange(cl, helper=None):
452 return _Query(opts, cl, raw=False, helper=helper)
453
454 transitives = _BreadthFirstSearch(
Mike Nicholsa1414162021-04-22 20:07:22 +0000455 cls, functools.partial(self._Children, opts, _QueryChange),
Mike Frysingerdc407f52020-05-08 00:34:56 -0400456 visited_key=lambda cl: cl.PatchLink())
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500457
Mike Frysingerdc407f52020-05-08 00:34:56 -0400458 # This is a hack to avoid losing GoB host for each CL. The PrintCls
459 # function assumes the GoB host specified by the user is the only one
460 # that is ever used, but the deps command walks across hosts.
Jack Rosenthale3a92672022-06-29 14:54:48 -0600461 if opts.format is OutputFormat.RAW:
Mike Frysingerdc407f52020-05-08 00:34:56 -0400462 print('\n'.join(x.PatchLink() for x in transitives))
463 else:
464 transitives_raw = [cl.patch_dict for cl in transitives]
465 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500466
467 @staticmethod
468 def _ProcessDeps(opts, querier, cl, deps, required):
Mike Frysinger5726da92017-09-20 22:14:25 -0400469 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700470 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400471 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400472 if not dep.remote in opts.gerrit:
473 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
474 remote=dep.remote, print_cmd=opts.debug)
475 helper = opts.gerrit[dep.remote]
476
Paul Hobbs89765232015-06-24 14:07:49 -0700477 # TODO(phobbs) this should maybe catch network errors.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500478 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400479
480 # Handle empty results. If we found a commit that was pushed directly
481 # (e.g. a bot commit), then gerrit won't know about it.
482 if not changes:
483 if required:
484 logging.error('CL %s depends on %s which cannot be found',
485 cl, dep.ToGerritQueryText())
486 continue
487
488 # Our query might have matched more than one result. This can come up
489 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
490 # across multiple repos/branches. We blindly check all of them in the
491 # hopes that all open ones are what the user wants, but then again the
Alex Kleinea9cc822022-05-25 12:39:48 -0600492 # CQ-DEPEND syntax itself is unable to differentiate. *shrug*
Mike Frysinger5726da92017-09-20 22:14:25 -0400493 if len(changes) > 1:
494 logging.warning('CL %s has an ambiguous CQ dependency %s',
495 cl, dep.ToGerritQueryText())
496 for change in changes:
497 if change.status == 'NEW':
498 yield change
499
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500500 @classmethod
Mike Nicholsa1414162021-04-22 20:07:22 +0000501 def _Children(cls, opts, querier, cl):
Mike Frysinger7cbd88c2021-02-12 03:52:25 -0500502 """Yields the Gerrit dependencies of a patch"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500503 for change in cls._ProcessDeps(
504 opts, querier, cl, cl.GerritDependencies(), False):
Mike Frysinger5726da92017-09-20 22:14:25 -0400505 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700506
Paul Hobbs89765232015-06-24 14:07:49 -0700507
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500508class ActionInspect(_ActionSearchQuery):
Harry Cutts26076b32019-02-26 15:01:29 -0800509 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500510
511 COMMAND = 'inspect'
512
513 @staticmethod
514 def init_subparser(parser):
515 """Add arguments to this action's subparser."""
516 _ActionSearchQuery.init_subparser(parser)
517 parser.add_argument('cls', nargs='+', metavar='CL',
518 help='The CL(s) to update')
519
520 @staticmethod
521 def __call__(opts):
522 """Implement the action."""
523 cls = []
524 for arg in opts.cls:
525 helper, cl = GetGerrit(opts, arg)
526 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
527 if change:
528 cls.extend(change)
529 else:
530 logging.warning('no results found for CL %s', arg)
531 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400532
533
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500534class _ActionLabeler(UserAction):
535 """Base helper for setting labels."""
536
537 LABEL = None
538 VALUES = None
539
540 @classmethod
541 def init_subparser(cls, parser):
542 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500543 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
544 help='Optional message to include')
545 parser.add_argument('cls', nargs='+', metavar='CL',
546 help='The CL(s) to update')
547 parser.add_argument('value', nargs=1, metavar='value', choices=cls.VALUES,
548 help='The label value; one of [%(choices)s]')
549
550 @classmethod
551 def __call__(cls, opts):
552 """Implement the action."""
Alex Kleinea9cc822022-05-25 12:39:48 -0600553 # Convert user-friendly command line option into a gerrit parameter.
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500554 def task(arg):
555 helper, cl = GetGerrit(opts, arg)
556 helper.SetReview(cl, labels={cls.LABEL: opts.value[0]}, msg=opts.msg,
557 dryrun=opts.dryrun, notify=opts.notify)
558 _run_parallel_tasks(task, *opts.cls)
559
560
561class ActionLabelAutoSubmit(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500562 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500563
564 COMMAND = 'label-as'
565 LABEL = 'Auto-Submit'
566 VALUES = ('0', '1')
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600567
568
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500569class ActionLabelCodeReview(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500570 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500571
572 COMMAND = 'label-cr'
573 LABEL = 'Code-Review'
574 VALUES = ('-2', '-1', '0', '1', '2')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400575
576
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500577class ActionLabelVerified(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500578 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500579
580 COMMAND = 'label-v'
581 LABEL = 'Verified'
582 VALUES = ('-1', '0', '1')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400583
584
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500585class ActionLabelCommitQueue(_ActionLabeler):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500586 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500587
588 COMMAND = 'label-cq'
589 LABEL = 'Commit-Queue'
590 VALUES = ('0', '1', '2')
Mike Frysinger15b23e42014-12-05 17:00:05 -0500591
C Shapiro3f1f8242021-08-02 15:28:29 -0500592class ActionLabelOwnersOverride(_ActionLabeler):
593 """Change the Owners-Override label (1=Override)"""
594
595 COMMAND = 'label-oo'
596 LABEL = 'Owners-Override'
597 VALUES = ('0', '1')
598
Mike Frysinger15b23e42014-12-05 17:00:05 -0500599
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500600class _ActionSimpleParallelCLs(UserAction):
601 """Base helper for actions that only accept CLs."""
602
603 @staticmethod
604 def init_subparser(parser):
605 """Add arguments to this action's subparser."""
606 parser.add_argument('cls', nargs='+', metavar='CL',
607 help='The CL(s) to update')
608
609 def __call__(self, opts):
610 """Implement the action."""
611 def task(arg):
612 helper, cl = GetGerrit(opts, arg)
613 self._process_one(helper, cl, opts)
614 _run_parallel_tasks(task, *opts.cls)
615
616
617class ActionSubmit(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800618 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500619
620 COMMAND = 'submit'
621
622 @staticmethod
623 def _process_one(helper, cl, opts):
624 """Use |helper| to process the single |cl|."""
Mike Frysinger8674a112021-02-09 14:44:17 -0500625 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400626
627
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500628class ActionAbandon(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800629 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500630
631 COMMAND = 'abandon'
632
633 @staticmethod
Mike Frysinger3af378b2021-03-12 01:34:04 -0500634 def init_subparser(parser):
635 """Add arguments to this action's subparser."""
636 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
637 help='Include a message')
638 _ActionSimpleParallelCLs.init_subparser(parser)
639
640 @staticmethod
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500641 def _process_one(helper, cl, opts):
642 """Use |helper| to process the single |cl|."""
Mike Frysinger3af378b2021-03-12 01:34:04 -0500643 helper.AbandonChange(cl, msg=opts.msg, dryrun=opts.dryrun,
644 notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400645
646
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500647class ActionRestore(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800648 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500649
650 COMMAND = 'restore'
651
652 @staticmethod
653 def _process_one(helper, cl, opts):
654 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700655 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400656
657
Tomasz Figa54d70992021-01-20 13:48:59 +0900658class ActionWorkInProgress(_ActionSimpleParallelCLs):
659 """Mark CLs as work in progress"""
660
661 COMMAND = 'wip'
662
663 @staticmethod
664 def _process_one(helper, cl, opts):
665 """Use |helper| to process the single |cl|."""
666 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
667
668
669class ActionReadyForReview(_ActionSimpleParallelCLs):
670 """Mark CLs as ready for review"""
671
672 COMMAND = 'ready'
673
674 @staticmethod
675 def _process_one(helper, cl, opts):
676 """Use |helper| to process the single |cl|."""
677 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
678
679
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500680class ActionReviewers(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800681 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700682
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500683 COMMAND = 'reviewers'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700684
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500685 @staticmethod
686 def init_subparser(parser):
687 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500688 parser.add_argument('cl', metavar='CL',
689 help='The CL to update')
690 parser.add_argument('reviewers', nargs='+',
691 help='The reviewers to add/remove')
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700692
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500693 @staticmethod
694 def __call__(opts):
695 """Implement the action."""
696 # Allow for optional leading '~'.
697 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
698 add_list, remove_list, invalid_list = [], [], []
699
700 for email in opts.reviewers:
701 if not email_validator.match(email):
702 invalid_list.append(email)
703 elif email[0] == '~':
704 remove_list.append(email[1:])
705 else:
706 add_list.append(email)
707
708 if invalid_list:
709 cros_build_lib.Die(
710 'Invalid email address(es): %s' % ', '.join(invalid_list))
711
712 if add_list or remove_list:
713 helper, cl = GetGerrit(opts, opts.cl)
714 helper.SetReviewers(cl, add=add_list, remove=remove_list,
715 dryrun=opts.dryrun, notify=opts.notify)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700716
717
Brian Norrisd25af082021-10-29 11:25:31 -0700718class ActionAttentionSet(UserAction):
719 """Add/remove emails from the attention set (prepend with '~' to remove)"""
720
721 COMMAND = 'attention'
722
723 @staticmethod
724 def init_subparser(parser):
725 """Add arguments to this action's subparser."""
726 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
727 help='Optional message to include',
728 default='gerrit CLI')
729 parser.add_argument('cl', metavar='CL',
730 help='The CL to update')
731 parser.add_argument('users', nargs='+',
732 help='The users to add/remove from attention set')
733
734 @staticmethod
735 def __call__(opts):
736 """Implement the action."""
737 # Allow for optional leading '~'.
738 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
739 add_list, remove_list, invalid_list = [], [], []
740
741 for email in opts.users:
742 if not email_validator.match(email):
743 invalid_list.append(email)
744 elif email[0] == '~':
745 remove_list.append(email[1:])
746 else:
747 add_list.append(email)
748
749 if invalid_list:
750 cros_build_lib.Die(
751 'Invalid email address(es): %s' % ', '.join(invalid_list))
752
753 if add_list or remove_list:
754 helper, cl = GetGerrit(opts, opts.cl)
755 helper.SetAttentionSet(cl, add=add_list, remove=remove_list,
756 dryrun=opts.dryrun, notify=opts.notify,
757 message=opts.msg)
758
759
Mike Frysinger62178ae2020-03-20 01:37:43 -0400760class ActionMessage(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800761 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500762
763 COMMAND = 'message'
764
765 @staticmethod
766 def init_subparser(parser):
767 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400768 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500769 parser.add_argument('message',
770 help='The message to post')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500771
772 @staticmethod
773 def _process_one(helper, cl, opts):
774 """Use |helper| to process the single |cl|."""
775 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530776
777
Mike Frysinger62178ae2020-03-20 01:37:43 -0400778class ActionTopic(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800779 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500780
781 COMMAND = 'topic'
782
783 @staticmethod
784 def init_subparser(parser):
785 """Add arguments to this action's subparser."""
Mike Frysinger62178ae2020-03-20 01:37:43 -0400786 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500787 parser.add_argument('topic',
788 help='The topic to set')
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500789
790 @staticmethod
791 def _process_one(helper, cl, opts):
792 """Use |helper| to process the single |cl|."""
793 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800794
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800795
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500796class ActionPrivate(_ActionSimpleParallelCLs):
797 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700798
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500799 COMMAND = 'private'
800
801 @staticmethod
802 def _process_one(helper, cl, opts):
803 """Use |helper| to process the single |cl|."""
804 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700805
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800806
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500807class ActionPublic(_ActionSimpleParallelCLs):
808 """Mark CLs public"""
809
810 COMMAND = 'public'
811
812 @staticmethod
813 def _process_one(helper, cl, opts):
814 """Use |helper| to process the single |cl|."""
815 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
816
817
818class ActionSethashtags(UserAction):
Harry Cutts26076b32019-02-26 15:01:29 -0800819 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500820
821 COMMAND = 'hashtags'
822
823 @staticmethod
824 def init_subparser(parser):
825 """Add arguments to this action's subparser."""
826 parser.add_argument('cl', metavar='CL',
827 help='The CL to update')
828 parser.add_argument('hashtags', nargs='+',
829 help='The hashtags to add/remove')
830
831 @staticmethod
832 def __call__(opts):
833 """Implement the action."""
834 add = []
835 remove = []
836 for hashtag in opts.hashtags:
837 if hashtag.startswith('~'):
838 remove.append(hashtag[1:])
839 else:
840 add.append(hashtag)
841 helper, cl = GetGerrit(opts, opts.cl)
842 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800843
844
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500845class ActionDeletedraft(_ActionSimpleParallelCLs):
Harry Cutts26076b32019-02-26 15:01:29 -0800846 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500847
848 COMMAND = 'deletedraft'
849
850 @staticmethod
851 def _process_one(helper, cl, opts):
852 """Use |helper| to process the single |cl|."""
Mike Frysinger88f27292014-06-17 09:40:45 -0700853 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800854
855
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500856class ActionReviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500857 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500858
859 COMMAND = 'reviewed'
860
861 @staticmethod
862 def _process_one(helper, cl, opts):
863 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500864 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500865
866
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500867class ActionUnreviewed(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500868 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500869
870 COMMAND = 'unreviewed'
871
872 @staticmethod
873 def _process_one(helper, cl, opts):
874 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500875 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500876
877
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500878class ActionIgnore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500879 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500880
881 COMMAND = 'ignore'
882
883 @staticmethod
884 def _process_one(helper, cl, opts):
885 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500886 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500887
888
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500889class ActionUnignore(_ActionSimpleParallelCLs):
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500890 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500891
892 COMMAND = 'unignore'
893
894 @staticmethod
895 def _process_one(helper, cl, opts):
896 """Use |helper| to process the single |cl|."""
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500897 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500898
899
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400900class ActionCherryPick(UserAction):
Alex Kleinea9cc822022-05-25 12:39:48 -0600901 """Cherry-pick CLs to branches."""
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400902
903 COMMAND = 'cherry-pick'
904
905 @staticmethod
906 def init_subparser(parser):
907 """Add arguments to this action's subparser."""
908 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
909 parser.add_argument('--rev', '--revision', default='current',
910 help='A specific revision or patchset')
911 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
912 help='Include a message')
913 parser.add_argument('--branches', '--branch', '--br', action='split_extend',
914 default=[], required=True,
915 help='The destination branches')
916 parser.add_argument('cls', nargs='+', metavar='CL',
917 help='The CLs to cherry-pick')
918
919 @staticmethod
920 def __call__(opts):
921 """Implement the action."""
922 # Process branches in parallel, but CLs in serial in case of CL stacks.
923 def task(branch):
924 for arg in opts.cls:
925 helper, cl = GetGerrit(opts, arg)
926 ret = helper.CherryPick(cl, branch, rev=opts.rev, msg=opts.msg,
Mike Frysinger8674a112021-02-09 14:44:17 -0500927 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400928 logging.debug('Response: %s', ret)
Jack Rosenthale3a92672022-06-29 14:54:48 -0600929 if opts.format is OutputFormat.RAW:
Mike Frysinger5dab15e2020-08-06 10:11:03 -0400930 print(ret['_number'])
931 else:
932 uri = f'https://{helper.host}/c/{ret["_number"]}'
933 print(uri_lib.ShortenUri(uri))
934
935 _run_parallel_tasks(task, *opts.branches)
936
937
Mike Frysinger8037f752020-02-29 20:47:09 -0500938class ActionReview(_ActionSimpleParallelCLs):
939 """Review CLs with multiple settings
940
941 The label option supports extended/multiple syntax for easy use. The --label
942 option may be specified multiple times (as settings are merges), and multiple
943 labels are allowed in a single argument. Each label has the form:
944 <long or short name><=+-><value>
945
946 Common arguments:
947 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
948 'V+1 CQ+2'
949 'AS=1 V=1'
950 """
951
952 COMMAND = 'review'
953
954 class _SetLabel(argparse.Action):
955 """Argparse action for setting labels."""
956
957 LABEL_MAP = {
958 'AS': 'Auto-Submit',
959 'CQ': 'Commit-Queue',
960 'CR': 'Code-Review',
961 'V': 'Verified',
962 }
963
964 def __call__(self, parser, namespace, values, option_string=None):
965 labels = getattr(namespace, self.dest)
966 for request in values.split():
967 if '=' in request:
968 # Handle Verified=1 form.
969 short, value = request.split('=', 1)
970 elif '+' in request:
971 # Handle Verified+1 form.
972 short, value = request.split('+', 1)
973 elif '-' in request:
974 # Handle Verified-1 form.
975 short, value = request.split('-', 1)
976 value = '-%s' % (value,)
977 else:
978 parser.error('Invalid label setting "%s". Must be Commit-Queue=1 or '
979 'CQ+1 or CR-1.' % (request,))
980
981 # Convert possible short label names like "V" to "Verified".
982 label = self.LABEL_MAP.get(short)
983 if not label:
984 label = short
985
986 # We allow existing label requests to be overridden.
987 labels[label] = value
988
989 @classmethod
990 def init_subparser(cls, parser):
991 """Add arguments to this action's subparser."""
992 parser.add_argument('-m', '--msg', '--message', metavar='MESSAGE',
993 help='Include a message')
994 parser.add_argument('-l', '--label', dest='labels',
995 action=cls._SetLabel, default={},
996 help='Set a label with a value')
997 parser.add_argument('--ready', default=None, action='store_true',
998 help='Set CL status to ready-for-review')
999 parser.add_argument('--wip', default=None, action='store_true',
1000 help='Set CL status to WIP')
1001 parser.add_argument('--reviewers', '--re', action='append', default=[],
1002 help='Add reviewers')
1003 parser.add_argument('--cc', action='append', default=[],
1004 help='Add people to CC')
1005 _ActionSimpleParallelCLs.init_subparser(parser)
1006
1007 @staticmethod
1008 def _process_one(helper, cl, opts):
1009 """Use |helper| to process the single |cl|."""
1010 helper.SetReview(cl, msg=opts.msg, labels=opts.labels, dryrun=opts.dryrun,
1011 notify=opts.notify, reviewers=opts.reviewers, cc=opts.cc,
1012 ready=opts.ready, wip=opts.wip)
1013
1014
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001015class ActionAccount(_ActionSimpleParallelCLs):
1016 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001017
1018 COMMAND = 'account'
Jack Rosenthal95aac172022-06-30 15:35:07 -06001019 USE_PAGER = True
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001020
1021 @staticmethod
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001022 def init_subparser(parser):
1023 """Add arguments to this action's subparser."""
1024 parser.add_argument('accounts', nargs='*', default=['self'],
1025 help='The accounts to query')
1026
1027 @classmethod
1028 def __call__(cls, opts):
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001029 """Implement the action."""
1030 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001031
1032 def print_one(header, data):
1033 print(f'### {header}')
Jack Rosenthale3a92672022-06-29 14:54:48 -06001034 compact = opts.format is OutputFormat.JSON
1035 print(pformat.json(data, compact=compact).rstrip())
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001036
1037 def task(arg):
1038 detail = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/detail')
1039 if not detail:
1040 print(f'{arg}: account not found')
1041 else:
1042 print_one('detail', detail)
1043 for field in ('groups', 'capabilities', 'preferences', 'sshkeys',
1044 'gpgkeys'):
1045 data = gob_util.FetchUrlJson(helper.host, f'accounts/{arg}/{field}')
1046 print_one(field, data)
1047
1048 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -08001049
1050
Mike Frysinger2295d792021-03-08 15:55:23 -05001051class ActionConfig(UserAction):
1052 """Manage the gerrit tool's own config file
1053
1054 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
1055 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
1056
1057 # Set up subcommand aliases.
1058 [alias]
1059 common-search = search 'is:open project:something/i/care/about'
1060 """
1061
1062 COMMAND = 'config'
1063
1064 @staticmethod
1065 def __call__(opts):
1066 """Implement the action."""
1067 # For now, this is a place holder for raising visibility for the config file
1068 # and its associated help text documentation.
1069 opts.parser.parse_args(['config', '--help'])
1070
1071
Mike Frysingere5450602021-03-08 15:34:17 -05001072class ActionHelp(UserAction):
1073 """An alias to --help for CLI symmetry"""
1074
1075 COMMAND = 'help'
Jack Rosenthal95aac172022-06-30 15:35:07 -06001076 USE_PAGER = True
Mike Frysingere5450602021-03-08 15:34:17 -05001077
1078 @staticmethod
1079 def init_subparser(parser):
1080 """Add arguments to this action's subparser."""
1081 parser.add_argument('command', nargs='?',
1082 help='The command to display.')
1083
1084 @staticmethod
1085 def __call__(opts):
1086 """Implement the action."""
1087 # Show global help.
1088 if not opts.command:
1089 opts.parser.print_help()
1090 return
1091
1092 opts.parser.parse_args([opts.command, '--help'])
1093
1094
Mike Frysinger484e2f82020-03-20 01:41:10 -04001095class ActionHelpAll(UserAction):
1096 """Show all actions help output at once."""
1097
1098 COMMAND = 'help-all'
Jack Rosenthal95aac172022-06-30 15:35:07 -06001099 USE_PAGER = True
Mike Frysinger484e2f82020-03-20 01:41:10 -04001100
1101 @staticmethod
1102 def __call__(opts):
1103 """Implement the action."""
1104 first = True
1105 for action in _GetActions():
1106 if first:
1107 first = False
1108 else:
1109 print('\n\n')
1110
1111 try:
1112 opts.parser.parse_args([action, '--help'])
1113 except SystemExit:
1114 pass
1115
1116
Mike Frysinger65fc8632020-02-06 18:11:12 -05001117@memoize.Memoize
1118def _GetActions():
1119 """Get all the possible actions we support.
1120
1121 Returns:
1122 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1123 function that implements that command (e.g. UserActFoo).
1124 """
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001125 VALID_NAME = re.compile(r'^[a-z][a-z-]*[a-z]$')
1126
1127 actions = {}
1128 for cls in globals().values():
1129 if (not inspect.isclass(cls) or
1130 not issubclass(cls, UserAction) or
1131 not getattr(cls, 'COMMAND', None)):
Mike Frysinger65fc8632020-02-06 18:11:12 -05001132 continue
1133
Mike Frysinger65fc8632020-02-06 18:11:12 -05001134 # Sanity check names for devs adding new commands. Should be quick.
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001135 cmd = cls.COMMAND
1136 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1137 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001138
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001139 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001140
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001141 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001142
1143
Harry Cutts26076b32019-02-26 15:01:29 -08001144def _GetActionUsages():
1145 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -05001146 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001147
Mike Frysinger65fc8632020-02-06 18:11:12 -05001148 cmds = list(actions.keys())
1149 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -08001150 usages = [getattr(x, 'usage', '') for x in functions]
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001151 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001152
Harry Cutts26076b32019-02-26 15:01:29 -08001153 cmd_indent = len(max(cmds, key=len))
1154 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001155 return '\n'.join(
1156 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
1157 for cmd, usage, doc in zip(cmds, usages, docs)
1158 )
Harry Cutts26076b32019-02-26 15:01:29 -08001159
1160
Mike Frysinger2295d792021-03-08 15:55:23 -05001161def _AddCommonOptions(parser, subparser):
1162 """Add options that should work before & after the subcommand.
1163
1164 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1165 """
1166 parser.add_common_argument_to_group(
1167 subparser, '--ne', '--no-emails', dest='notify',
1168 default='ALL', action='store_const', const='NONE',
1169 help='Do not send e-mail notifications')
1170 parser.add_common_argument_to_group(
1171 subparser, '-n', '--dry-run', dest='dryrun',
1172 default=False, action='store_true',
1173 help='Show what would be done, but do not make changes')
1174
1175
1176def GetBaseParser() -> commandline.ArgumentParser:
1177 """Returns the common parser (i.e. no subparsers added)."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001178 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001179There is no support for doing line-by-line code review via the command line.
1180This helps you manage various bits and CL status.
1181
Mike Frysingera1db2c42014-06-15 00:42:48 -07001182For general Gerrit documentation, see:
1183 https://gerrit-review.googlesource.com/Documentation/
1184The Searching Changes page covers the search query syntax:
1185 https://gerrit-review.googlesource.com/Documentation/user-search.html
1186
Mike Frysinger13f23a42013-05-13 17:32:01 -04001187Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001188 $ gerrit todo # List all the CLs that await your review.
1189 $ gerrit mine # List all of your open CLs.
1190 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1191 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1192 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001193 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
119428123.
1195 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1196CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001197Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001198 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1199with Commit-Queue=1.
1200 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1201CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001202 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001203
Harry Cutts26076b32019-02-26 15:01:29 -08001204Actions:
1205"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001206 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001207
Alex Klein2ab29cc2018-07-19 12:01:00 -06001208 site_params = config_lib.GetSiteParams()
Mike Frysinger3f257c82020-06-16 01:42:29 -04001209 parser = commandline.ArgumentParser(
Mike Frysinger50917ad2022-04-12 20:37:14 -04001210 description=description, default_log_level='notice',
1211 epilog='For subcommand help, use `gerrit help <command>`.')
Mike Frysinger8674a112021-02-09 14:44:17 -05001212
1213 group = parser.add_argument_group('Server options')
1214 group.add_argument('-i', '--internal', dest='gob', action='store_const',
1215 default=site_params.EXTERNAL_GOB_INSTANCE,
1216 const=site_params.INTERNAL_GOB_INSTANCE,
1217 help='Query internal Chrome Gerrit instance')
1218 group.add_argument('-g', '--gob',
1219 default=site_params.EXTERNAL_GOB_INSTANCE,
Brian Norrisd25af082021-10-29 11:25:31 -07001220 help=('Gerrit (on borg) instance to query '
1221 '(default: %(default)s)'))
Mike Frysinger8674a112021-02-09 14:44:17 -05001222
Mike Frysinger8674a112021-02-09 14:44:17 -05001223 group = parser.add_argument_group('CL options')
Mike Frysinger2295d792021-03-08 15:55:23 -05001224 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001225
Jack Rosenthale3a92672022-06-29 14:54:48 -06001226 group = parser.add_mutually_exclusive_group()
1227 parser.set_defaults(format=OutputFormat.AUTO)
1228 group.add_argument(
1229 '--format',
1230 action='enum',
1231 enum=OutputFormat,
1232 help='Output format to use.',
1233 )
1234 group.add_argument(
1235 '--raw',
1236 action='store_const',
1237 dest='format',
1238 const=OutputFormat.RAW,
1239 help='Alias for --format=raw.',
1240 )
1241 group.add_argument(
1242 '--json',
1243 action='store_const',
1244 dest='format',
1245 const=OutputFormat.JSON,
1246 help='Alias for --format=json.',
1247 )
Jack Rosenthal95aac172022-06-30 15:35:07 -06001248
1249 group = parser.add_mutually_exclusive_group()
1250 group.add_argument(
1251 '--pager',
1252 action='store_true',
1253 default=sys.stdout.isatty(),
1254 help='Enable pager.',
1255 )
1256 group.add_argument(
1257 '--no-pager',
1258 action='store_false',
1259 dest='pager',
1260 help='Disable pager.'
1261 )
Mike Frysinger2295d792021-03-08 15:55:23 -05001262 return parser
1263
1264
1265def GetParser(parser: commandline.ArgumentParser = None) -> (
1266 commandline.ArgumentParser):
1267 """Returns the full parser to use for this module."""
1268 if parser is None:
1269 parser = GetBaseParser()
1270
1271 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001272
1273 # Subparsers are required by default under Python 2. Python 3 changed to
1274 # not required, but didn't include a required option until 3.7. Setting
1275 # the required member works in all versions (and setting dest name).
1276 subparsers = parser.add_subparsers(dest='action')
1277 subparsers.required = True
1278 for cmd, cls in actions.items():
1279 # Format the full docstring by removing the file level indentation.
1280 description = re.sub(r'^ ', '', cls.__doc__, flags=re.M)
1281 subparser = subparsers.add_parser(cmd, description=description)
Mike Frysinger2295d792021-03-08 15:55:23 -05001282 _AddCommonOptions(parser, subparser)
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001283 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001284
1285 return parser
1286
1287
Jack Rosenthal95aac172022-06-30 15:35:07 -06001288def start_pager():
1289 """Re-spawn ourselves attached to a pager."""
1290 pager = os.environ.get('PAGER', 'less')
1291 os.environ.setdefault('LESS', 'FRX')
1292 with subprocess.Popen(
1293 # sys.argv can have some edge cases: we may not necessarily use
1294 # sys.executable if the script is executed as "python path/to/script".
1295 # If we upgrade to Python 3.10+, this should be changed to sys.orig_argv
1296 # for full accuracy.
1297 sys.argv,
1298 stdout=subprocess.PIPE,
1299 stderr=subprocess.STDOUT,
1300 env={'GERRIT_RESPAWN_FOR_PAGER': '1', **os.environ},
1301 ) as gerrit_proc:
1302 with subprocess.Popen(
1303 pager,
1304 shell=True,
1305 stdin=gerrit_proc.stdout,
1306 ) as pager_proc:
1307 # Send SIGINT to just the gerrit process, not the pager too.
1308 def _sighandler(signum, _frame):
1309 gerrit_proc.send_signal(signum)
1310
1311 signal.signal(signal.SIGINT, _sighandler)
1312
1313 pager_proc.communicate()
1314 # If the pager exits, and the gerrit process is still running, we
1315 # must terminate it.
1316 if gerrit_proc.poll() is None:
1317 gerrit_proc.terminate()
1318 sys.exit(gerrit_proc.wait())
1319
1320
Mike Frysinger108eda22018-06-06 18:45:12 -04001321def main(argv):
Mike Frysinger2295d792021-03-08 15:55:23 -05001322 base_parser = GetBaseParser()
1323 opts, subargs = base_parser.parse_known_args(argv)
1324
1325 config = Config()
1326 if subargs:
1327 # If the action is an alias to an expanded value, we need to mutate the argv
1328 # and reparse things.
1329 action = config.expand_alias(subargs[0])
1330 if action != subargs[0]:
1331 pos = argv.index(subargs[0])
1332 argv = argv[:pos] + action + argv[pos + 1:]
1333
1334 parser = GetParser(parser=base_parser)
Mike Frysingerddf86eb2014-02-07 22:51:41 -05001335 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001336
Jack Rosenthal95aac172022-06-30 15:35:07 -06001337 # If we're running as a re-spawn for the pager, from this point on
1338 # we'll pretend we're attached to a TTY. This will give us colored
1339 # output when requested.
1340 if os.environ.pop('GERRIT_RESPAWN_FOR_PAGER', None) is not None:
1341 opts.pager = False
1342 sys.stdout.isatty = lambda: True
1343
Mike Frysinger484e2f82020-03-20 01:41:10 -04001344 # In case the action wants to throw a parser error.
1345 opts.parser = parser
1346
Mike Frysinger88f27292014-06-17 09:40:45 -07001347 # A cache of gerrit helpers we'll load on demand.
1348 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001349
Jack Rosenthale3a92672022-06-29 14:54:48 -06001350 if opts.format is OutputFormat.AUTO:
Jack Rosenthal6f4ed452022-06-30 20:07:02 -06001351 if sys.stdout.isatty():
1352 opts.format = OutputFormat.PRETTY
1353 else:
1354 opts.format = OutputFormat.RAW
Jack Rosenthale3a92672022-06-29 14:54:48 -06001355
Mike Frysinger88f27292014-06-17 09:40:45 -07001356 opts.Freeze()
1357
Mike Frysinger27e21b72018-07-12 14:20:21 -04001358 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001359 global COLOR
1360 COLOR = terminal.Color(enabled=opts.color)
1361
Mike Frysinger13f23a42013-05-13 17:32:01 -04001362 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001363 actions = _GetActions()
Jack Rosenthal95aac172022-06-30 15:35:07 -06001364 action_class = actions[opts.action]
1365 if action_class.USE_PAGER and opts.pager:
1366 start_pager()
1367 obj = action_class()
Mike Frysinger65fc8632020-02-06 18:11:12 -05001368 try:
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001369 obj(opts)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001370 except (cros_build_lib.RunCommandError, gerrit.GerritException,
1371 gob_util.GOBError) as e:
1372 cros_build_lib.Die(e)