blob: 5de9777ae754174440afb40c3f61a7d1faa674b5 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2012 The ChromiumOS Authors
Mike Frysinger13f23a42013-05-13 17:32:01 -04002# 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:
Alex Klein1699fab2022-09-08 08:46:06 -060044 """Manage the user's gerrit config settings.
Mike Frysinger2295d792021-03-08 15:55:23 -050045
Alex Klein1699fab2022-09-08 08:46:06 -060046 This is entirely unique to this gerrit command. Inspiration for naming and
47 layout is taken from ~/.gitconfig settings.
48 """
Mike Frysinger2295d792021-03-08 15:55:23 -050049
Alex Klein1699fab2022-09-08 08:46:06 -060050 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)
Mike Frysinger2295d792021-03-08 15:55:23 -050054
Alex Klein1699fab2022-09-08 08:46:06 -060055 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
Mike Frysinger2295d792021-03-08 15:55:23 -050061
62
Mike Frysingerc7796cf2020-02-06 23:55:15 -050063class UserAction(object):
Alex Klein1699fab2022-09-08 08:46:06 -060064 """Base class for all custom user actions."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -050065
Alex Klein1699fab2022-09-08 08:46:06 -060066 # The name of the command the user types in.
67 COMMAND = None
Mike Frysingerc7796cf2020-02-06 23:55:15 -050068
Alex Klein1699fab2022-09-08 08:46:06 -060069 # Should output be paged?
70 USE_PAGER = False
Jack Rosenthal95aac172022-06-30 15:35:07 -060071
Alex Klein1699fab2022-09-08 08:46:06 -060072 @staticmethod
73 def init_subparser(parser):
74 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -050075
Alex Klein1699fab2022-09-08 08:46:06 -060076 @staticmethod
77 def __call__(opts):
78 """Implement the action."""
79 raise RuntimeError(
80 "Internal error: action missing __call__ implementation"
81 )
Mike Frysinger108eda22018-06-06 18:45:12 -040082
83
Mike Frysinger254f33f2019-12-11 13:54:29 -050084# How many connections we'll use in parallel. We don't want this to be too high
85# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
86# seems to be good enough for users.
87CONNECTION_LIMIT = 10
88
89
Mike Frysinger031ad0b2013-05-14 18:15:34 -040090COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040091
92# Map the internal names to the ones we normally show on the web ui.
93GERRIT_APPROVAL_MAP = {
Alex Klein1699fab2022-09-08 08:46:06 -060094 "COMR": [
95 "CQ",
96 "Commit Queue ",
97 ],
98 "CRVW": [
99 "CR",
100 "Code Review ",
101 ],
102 "SUBM": [
103 "S ",
104 "Submitted ",
105 ],
106 "VRIF": [
107 "V ",
108 "Verified ",
109 ],
110 "LCQ": [
111 "L ",
112 "Legacy ",
113 ],
Mike Frysinger13f23a42013-05-13 17:32:01 -0400114}
115
116# Order is important -- matches the web ui. This also controls the short
117# entries that we summarize in non-verbose mode.
Alex Klein1699fab2022-09-08 08:46:06 -0600118GERRIT_SUMMARY_CATS = (
119 "CR",
120 "CQ",
121 "V",
122)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400123
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400124# Shorter strings for CL status messages.
125GERRIT_SUMMARY_MAP = {
Alex Klein1699fab2022-09-08 08:46:06 -0600126 "ABANDONED": "ABD",
127 "MERGED": "MRG",
128 "NEW": "NEW",
129 "WIP": "WIP",
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400130}
131
Mike Frysinger13f23a42013-05-13 17:32:01 -0400132
Jack Rosenthale3a92672022-06-29 14:54:48 -0600133class OutputFormat(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -0600134 """Type for the requested output format.
Jack Rosenthale3a92672022-06-29 14:54:48 -0600135
Alex Klein1699fab2022-09-08 08:46:06 -0600136 AUTO: Automatically determine the format based on what the user
137 might want. This is PRETTY if attached to a terminal, RAW
138 otherwise.
139 RAW: Output CLs one per line, suitable for mild scripting.
140 JSON: JSON-encoded output, suitable for spicy scripting.
141 MARKDOWN: Suitable for posting in a bug or CL comment.
142 PRETTY: Suitable for viewing in a color terminal.
143 """
144
145 AUTO = 0
146 AUTOMATIC = AUTO
147 RAW = 1
148 JSON = 2
149 MARKDOWN = 3
150 PRETTY = 4
Jack Rosenthale3a92672022-06-29 14:54:48 -0600151
152
Mike Frysinger13f23a42013-05-13 17:32:01 -0400153def red(s):
Alex Klein1699fab2022-09-08 08:46:06 -0600154 return COLOR.Color(terminal.Color.RED, s)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400155
156
157def green(s):
Alex Klein1699fab2022-09-08 08:46:06 -0600158 return COLOR.Color(terminal.Color.GREEN, s)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400159
160
161def blue(s):
Alex Klein1699fab2022-09-08 08:46:06 -0600162 return COLOR.Color(terminal.Color.BLUE, s)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400163
164
Mike Frysinger254f33f2019-12-11 13:54:29 -0500165def _run_parallel_tasks(task, *args):
Alex Klein1699fab2022-09-08 08:46:06 -0600166 """Small wrapper around BackgroundTaskRunner to enforce job count."""
167 # When we run in parallel, we can hit the max requests limit.
168 def check_exc(e):
169 if not isinstance(e, gob_util.GOBError):
170 raise e
171 return e.http_status == 429
Mike Frysingera9751c92021-04-30 10:12:37 -0400172
Alex Klein1699fab2022-09-08 08:46:06 -0600173 @retry_util.WithRetry(5, handler=check_exc, sleep=1, backoff_factor=2)
174 def retry(*args):
175 try:
176 task(*args)
177 except gob_util.GOBError as e:
178 if e.http_status != 429:
179 logging.warning("%s: skipping due: %s", args, e)
180 else:
181 raise
Mike Frysingera9751c92021-04-30 10:12:37 -0400182
Alex Klein1699fab2022-09-08 08:46:06 -0600183 with parallel.BackgroundTaskRunner(retry, processes=CONNECTION_LIMIT) as q:
184 for arg in args:
185 q.put([arg])
Mike Frysinger254f33f2019-12-11 13:54:29 -0500186
187
Mike Frysinger13f23a42013-05-13 17:32:01 -0400188def limits(cls):
Alex Klein1699fab2022-09-08 08:46:06 -0600189 """Given a dict of fields, calculate the longest string lengths
Mike Frysinger13f23a42013-05-13 17:32:01 -0400190
Alex Klein1699fab2022-09-08 08:46:06 -0600191 This allows you to easily format the output of many results so that the
192 various cols all line up correctly.
193 """
194 lims = {}
195 for cl in cls:
196 for k in cl.keys():
197 # Use %s rather than str() to avoid codec issues.
198 # We also do this so we can format integers.
199 lims[k] = max(lims.get(k, 0), len("%s" % cl[k]))
200 return lims
Mike Frysinger13f23a42013-05-13 17:32:01 -0400201
202
Mike Frysinger88f27292014-06-17 09:40:45 -0700203# TODO: This func really needs to be merged into the core gerrit logic.
204def GetGerrit(opts, cl=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600205 """Auto pick the right gerrit instance based on the |cl|
Mike Frysinger88f27292014-06-17 09:40:45 -0700206
Alex Klein1699fab2022-09-08 08:46:06 -0600207 Args:
208 opts: The general options object.
209 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
Mike Frysinger88f27292014-06-17 09:40:45 -0700210
Alex Klein1699fab2022-09-08 08:46:06 -0600211 Returns:
212 A tuple of a gerrit object and a sanitized CL #.
213 """
214 gob = opts.gob
215 if cl is not None:
216 if cl.startswith("*") or cl.startswith("chrome-internal:"):
217 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
218 if cl.startswith("*"):
219 cl = cl[1:]
220 else:
221 cl = cl[16:]
222 elif ":" in cl:
223 gob, cl = cl.split(":", 1)
Mike Frysinger88f27292014-06-17 09:40:45 -0700224
Alex Klein1699fab2022-09-08 08:46:06 -0600225 if not gob in opts.gerrit:
226 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
Mike Frysinger88f27292014-06-17 09:40:45 -0700227
Alex Klein1699fab2022-09-08 08:46:06 -0600228 return (opts.gerrit[gob], cl)
Mike Frysinger88f27292014-06-17 09:40:45 -0700229
230
Mike Frysinger13f23a42013-05-13 17:32:01 -0400231def GetApprovalSummary(_opts, cls):
Alex Klein1699fab2022-09-08 08:46:06 -0600232 """Return a dict of the most important approvals"""
233 approvs = dict([(x, "") for x in GERRIT_SUMMARY_CATS])
234 for approver in cls.get("currentPatchSet", {}).get("approvals", []):
235 cats = GERRIT_APPROVAL_MAP.get(approver["type"])
236 if not cats:
237 logging.warning(
238 "unknown gerrit approval type: %s", approver["type"]
239 )
240 continue
241 cat = cats[0].strip()
242 val = int(approver["value"])
243 if not cat in approvs:
244 # Ignore the extended categories in the summary view.
245 continue
246 elif approvs[cat] == "":
247 approvs[cat] = val
248 elif val < 0:
249 approvs[cat] = min(approvs[cat], val)
250 else:
251 approvs[cat] = max(approvs[cat], val)
252 return approvs
Mike Frysinger13f23a42013-05-13 17:32:01 -0400253
254
Mike Frysingera1b4b272017-04-05 16:11:00 -0400255def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600256 """Pretty print a single result"""
257 if lims is None:
258 lims = {"url": 0, "project": 0}
Mike Frysinger13f23a42013-05-13 17:32:01 -0400259
Alex Klein1699fab2022-09-08 08:46:06 -0600260 status = ""
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400261
Alex Klein1699fab2022-09-08 08:46:06 -0600262 if opts.verbose:
263 status += "%s " % (cl["status"],)
264 else:
265 status += "%s " % (GERRIT_SUMMARY_MAP.get(cl["status"], cl["status"]),)
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400266
Alex Klein1699fab2022-09-08 08:46:06 -0600267 if show_approvals and not opts.verbose:
268 approvs = GetApprovalSummary(opts, cl)
269 for cat in GERRIT_SUMMARY_CATS:
270 if approvs[cat] in ("", 0):
271 functor = lambda x: x
272 elif approvs[cat] < 0:
273 functor = red
274 else:
275 functor = green
276 status += functor("%s:%2s " % (cat, approvs[cat]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400277
Alex Klein1699fab2022-09-08 08:46:06 -0600278 if opts.format is OutputFormat.MARKDOWN:
279 print("* %s - %s" % (uri_lib.ShortenUri(cl["url"]), cl["subject"]))
280 else:
281 print(
282 "%s %s%-*s %s"
283 % (
284 blue("%-*s" % (lims["url"], cl["url"])),
285 status,
286 lims["project"],
287 cl["project"],
288 cl["subject"],
289 )
290 )
Mike Frysinger13f23a42013-05-13 17:32:01 -0400291
Alex Klein1699fab2022-09-08 08:46:06 -0600292 if show_approvals and opts.verbose:
293 for approver in cl["currentPatchSet"].get("approvals", []):
294 functor = red if int(approver["value"]) < 0 else green
295 n = functor("%2s" % approver["value"])
296 t = GERRIT_APPROVAL_MAP.get(
297 approver["type"], [approver["type"], approver["type"]]
298 )[1]
299 print(" %s %s %s" % (n, t, approver["by"]["email"]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400300
301
Mike Frysingera1b4b272017-04-05 16:11:00 -0400302def PrintCls(opts, cls, lims=None, show_approvals=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600303 """Print all results based on the requested format."""
304 if opts.format is OutputFormat.RAW:
305 site_params = config_lib.GetSiteParams()
306 pfx = ""
307 # Special case internal Chrome GoB as that is what most devs use.
308 # They can always redirect the list elsewhere via the -g option.
309 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
310 pfx = site_params.INTERNAL_CHANGE_PREFIX
311 for cl in cls:
312 print("%s%s" % (pfx, cl["number"]))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400313
Alex Klein1699fab2022-09-08 08:46:06 -0600314 elif opts.format is OutputFormat.JSON:
315 json.dump(cls, sys.stdout)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400316
Alex Klein1699fab2022-09-08 08:46:06 -0600317 else:
318 if lims is None:
319 lims = limits(cls)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400320
Alex Klein1699fab2022-09-08 08:46:06 -0600321 for cl in cls:
322 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400323
324
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400325def _Query(opts, query, raw=True, helper=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600326 """Queries Gerrit with a query string built from the commandline options"""
327 if opts.branch is not None:
328 query += " branch:%s" % opts.branch
329 if opts.project is not None:
330 query += " project: %s" % opts.project
331 if opts.topic is not None:
332 query += " topic: %s" % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800333
Alex Klein1699fab2022-09-08 08:46:06 -0600334 if helper is None:
335 helper, _ = GetGerrit(opts)
336 return helper.Query(query, raw=raw, bypass_cache=False)
Paul Hobbs89765232015-06-24 14:07:49 -0700337
338
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400339def FilteredQuery(opts, query, helper=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600340 """Query gerrit and filter/clean up the results"""
341 ret = []
Paul Hobbs89765232015-06-24 14:07:49 -0700342
Alex Klein1699fab2022-09-08 08:46:06 -0600343 logging.debug("Running query: %s", query)
344 for cl in _Query(opts, query, raw=True, helper=helper):
345 # Gerrit likes to return a stats record too.
346 if not "project" in cl:
347 continue
Mike Frysinger13f23a42013-05-13 17:32:01 -0400348
Alex Klein1699fab2022-09-08 08:46:06 -0600349 # Strip off common leading names since the result is still
350 # unique over the whole tree.
351 if not opts.verbose:
352 for pfx in (
353 "aosp",
354 "chromeos",
355 "chromiumos",
356 "external",
357 "overlays",
358 "platform",
359 "third_party",
360 ):
361 if cl["project"].startswith("%s/" % pfx):
362 cl["project"] = cl["project"][len(pfx) + 1 :]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400363
Alex Klein1699fab2022-09-08 08:46:06 -0600364 cl["url"] = uri_lib.ShortenUri(cl["url"])
Mike Frysinger479f1192017-09-14 22:36:30 -0400365
Alex Klein1699fab2022-09-08 08:46:06 -0600366 ret.append(cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400367
Alex Klein1699fab2022-09-08 08:46:06 -0600368 if opts.sort == "unsorted":
369 return ret
370 if opts.sort == "number":
371 key = lambda x: int(x[opts.sort])
372 else:
373 key = lambda x: x[opts.sort]
374 return sorted(ret, key=key)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400375
376
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500377class _ActionSearchQuery(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600378 """Base class for actions that perform searches."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500379
Alex Klein1699fab2022-09-08 08:46:06 -0600380 USE_PAGER = True
Jack Rosenthal95aac172022-06-30 15:35:07 -0600381
Alex Klein1699fab2022-09-08 08:46:06 -0600382 @staticmethod
383 def init_subparser(parser):
384 """Add arguments to this action's subparser."""
385 parser.add_argument(
386 "--sort",
387 default="number",
388 help='Key to sort on (number, project); use "unsorted" '
389 "to disable",
390 )
391 parser.add_argument(
392 "-b", "--branch", help="Limit output to the specific branch"
393 )
394 parser.add_argument(
395 "-p", "--project", help="Limit output to the specific project"
396 )
397 parser.add_argument(
398 "-t", "--topic", help="Limit output to the specific topic"
399 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500400
401
402class ActionTodo(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600403 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500404
Alex Klein1699fab2022-09-08 08:46:06 -0600405 COMMAND = "todo"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500406
Alex Klein1699fab2022-09-08 08:46:06 -0600407 @staticmethod
408 def __call__(opts):
409 """Implement the action."""
410 cls = FilteredQuery(opts, "attention:self")
411 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400412
413
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500414class ActionSearch(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600415 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500416
Alex Klein1699fab2022-09-08 08:46:06 -0600417 COMMAND = "search"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500418
Alex Klein1699fab2022-09-08 08:46:06 -0600419 @staticmethod
420 def init_subparser(parser):
421 """Add arguments to this action's subparser."""
422 _ActionSearchQuery.init_subparser(parser)
423 parser.add_argument("query", help="The search query")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500424
Alex Klein1699fab2022-09-08 08:46:06 -0600425 @staticmethod
426 def __call__(opts):
427 """Implement the action."""
428 cls = FilteredQuery(opts, opts.query)
429 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400430
431
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500432class ActionMine(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600433 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500434
Alex Klein1699fab2022-09-08 08:46:06 -0600435 COMMAND = "mine"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500436
Alex Klein1699fab2022-09-08 08:46:06 -0600437 @staticmethod
438 def init_subparser(parser):
439 """Add arguments to this action's subparser."""
440 _ActionSearchQuery.init_subparser(parser)
441 parser.add_argument(
442 "--draft",
443 default=False,
444 action="store_true",
445 help="Show draft changes",
446 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500447
Alex Klein1699fab2022-09-08 08:46:06 -0600448 @staticmethod
449 def __call__(opts):
450 """Implement the action."""
451 if opts.draft:
452 rule = "is:draft"
453 else:
454 rule = "status:new"
455 cls = FilteredQuery(opts, "owner:self %s" % (rule,))
456 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700457
458
Paul Hobbs89765232015-06-24 14:07:49 -0700459def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
Alex Klein1699fab2022-09-08 08:46:06 -0600460 """Runs breadth first search starting from the nodes in |to_visit|
Paul Hobbs89765232015-06-24 14:07:49 -0700461
Alex Klein1699fab2022-09-08 08:46:06 -0600462 Args:
463 to_visit: the starting nodes
464 children: a function which takes a node and returns the nodes adjacent to it
465 visited_key: a function for deduplicating node visits. Defaults to the
466 identity function (lambda x: x)
Paul Hobbs89765232015-06-24 14:07:49 -0700467
Alex Klein1699fab2022-09-08 08:46:06 -0600468 Returns:
469 A list of nodes which are reachable from any node in |to_visit| by calling
470 |children| any number of times.
471 """
472 to_visit = list(to_visit)
473 seen = set(visited_key(x) for x in to_visit)
474 for node in to_visit:
475 for child in children(node):
476 key = visited_key(child)
477 if key not in seen:
478 seen.add(key)
479 to_visit.append(child)
480 return to_visit
Paul Hobbs89765232015-06-24 14:07:49 -0700481
482
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500483class ActionDeps(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600484 """List CLs matching a query, and all transitive dependencies of those CLs"""
Paul Hobbs89765232015-06-24 14:07:49 -0700485
Alex Klein1699fab2022-09-08 08:46:06 -0600486 COMMAND = "deps"
Paul Hobbs89765232015-06-24 14:07:49 -0700487
Alex Klein1699fab2022-09-08 08:46:06 -0600488 @staticmethod
489 def init_subparser(parser):
490 """Add arguments to this action's subparser."""
491 _ActionSearchQuery.init_subparser(parser)
492 parser.add_argument("query", help="The search query")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500493
Alex Klein1699fab2022-09-08 08:46:06 -0600494 def __call__(self, opts):
495 """Implement the action."""
496 cls = _Query(opts, opts.query, raw=False)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500497
Alex Klein1699fab2022-09-08 08:46:06 -0600498 @memoize.Memoize
499 def _QueryChange(cl, helper=None):
500 return _Query(opts, cl, raw=False, helper=helper)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500501
Alex Klein1699fab2022-09-08 08:46:06 -0600502 transitives = _BreadthFirstSearch(
503 cls,
504 functools.partial(self._Children, opts, _QueryChange),
505 visited_key=lambda cl: cl.PatchLink(),
506 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500507
Alex Klein1699fab2022-09-08 08:46:06 -0600508 # This is a hack to avoid losing GoB host for each CL. The PrintCls
509 # function assumes the GoB host specified by the user is the only one
510 # that is ever used, but the deps command walks across hosts.
511 if opts.format is OutputFormat.RAW:
512 print("\n".join(x.PatchLink() for x in transitives))
513 else:
514 transitives_raw = [cl.patch_dict for cl in transitives]
515 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500516
Alex Klein1699fab2022-09-08 08:46:06 -0600517 @staticmethod
518 def _ProcessDeps(opts, querier, cl, deps, required):
519 """Yields matching dependencies for a patch"""
520 # We need to query the change to guarantee that we have a .gerrit_number
521 for dep in deps:
522 if not dep.remote in opts.gerrit:
523 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
524 remote=dep.remote, print_cmd=opts.debug
525 )
526 helper = opts.gerrit[dep.remote]
Mike Frysingerb3300c42017-07-20 01:41:17 -0400527
Alex Klein1699fab2022-09-08 08:46:06 -0600528 # TODO(phobbs) this should maybe catch network errors.
529 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400530
Alex Klein1699fab2022-09-08 08:46:06 -0600531 # Handle empty results. If we found a commit that was pushed directly
532 # (e.g. a bot commit), then gerrit won't know about it.
533 if not changes:
534 if required:
535 logging.error(
536 "CL %s depends on %s which cannot be found",
537 cl,
538 dep.ToGerritQueryText(),
539 )
540 continue
Mike Frysinger5726da92017-09-20 22:14:25 -0400541
Alex Klein1699fab2022-09-08 08:46:06 -0600542 # Our query might have matched more than one result. This can come up
543 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
544 # across multiple repos/branches. We blindly check all of them in the
545 # hopes that all open ones are what the user wants, but then again the
546 # CQ-DEPEND syntax itself is unable to differentiate. *shrug*
547 if len(changes) > 1:
548 logging.warning(
549 "CL %s has an ambiguous CQ dependency %s",
550 cl,
551 dep.ToGerritQueryText(),
552 )
553 for change in changes:
554 if change.status == "NEW":
555 yield change
Mike Frysinger5726da92017-09-20 22:14:25 -0400556
Alex Klein1699fab2022-09-08 08:46:06 -0600557 @classmethod
558 def _Children(cls, opts, querier, cl):
559 """Yields the Gerrit dependencies of a patch"""
560 for change in cls._ProcessDeps(
561 opts, querier, cl, cl.GerritDependencies(), False
562 ):
563 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700564
Paul Hobbs89765232015-06-24 14:07:49 -0700565
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500566class ActionInspect(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600567 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500568
Alex Klein1699fab2022-09-08 08:46:06 -0600569 COMMAND = "inspect"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500570
Alex Klein1699fab2022-09-08 08:46:06 -0600571 @staticmethod
572 def init_subparser(parser):
573 """Add arguments to this action's subparser."""
574 _ActionSearchQuery.init_subparser(parser)
575 parser.add_argument(
576 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
577 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500578
Alex Klein1699fab2022-09-08 08:46:06 -0600579 @staticmethod
580 def __call__(opts):
581 """Implement the action."""
582 cls = []
583 for arg in opts.cls:
584 helper, cl = GetGerrit(opts, arg)
585 change = FilteredQuery(opts, "change:%s" % cl, helper=helper)
586 if change:
587 cls.extend(change)
588 else:
589 logging.warning("no results found for CL %s", arg)
590 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400591
592
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500593class _ActionLabeler(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600594 """Base helper for setting labels."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500595
Alex Klein1699fab2022-09-08 08:46:06 -0600596 LABEL = None
597 VALUES = None
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500598
Alex Klein1699fab2022-09-08 08:46:06 -0600599 @classmethod
600 def init_subparser(cls, parser):
601 """Add arguments to this action's subparser."""
602 parser.add_argument(
603 "-m",
604 "--msg",
605 "--message",
606 metavar="MESSAGE",
607 help="Optional message to include",
608 )
609 parser.add_argument(
610 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
611 )
612 parser.add_argument(
613 "value",
614 nargs=1,
615 metavar="value",
616 choices=cls.VALUES,
617 help="The label value; one of [%(choices)s]",
618 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500619
Alex Klein1699fab2022-09-08 08:46:06 -0600620 @classmethod
621 def __call__(cls, opts):
622 """Implement the action."""
623 # Convert user-friendly command line option into a gerrit parameter.
624 def task(arg):
625 helper, cl = GetGerrit(opts, arg)
626 helper.SetReview(
627 cl,
628 labels={cls.LABEL: opts.value[0]},
629 msg=opts.msg,
630 dryrun=opts.dryrun,
631 notify=opts.notify,
632 )
633
634 _run_parallel_tasks(task, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500635
636
637class ActionLabelAutoSubmit(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600638 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500639
Alex Klein1699fab2022-09-08 08:46:06 -0600640 COMMAND = "label-as"
641 LABEL = "Auto-Submit"
642 VALUES = ("0", "1")
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600643
644
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500645class ActionLabelCodeReview(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600646 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500647
Alex Klein1699fab2022-09-08 08:46:06 -0600648 COMMAND = "label-cr"
649 LABEL = "Code-Review"
650 VALUES = ("-2", "-1", "0", "1", "2")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400651
652
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500653class ActionLabelVerified(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600654 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500655
Alex Klein1699fab2022-09-08 08:46:06 -0600656 COMMAND = "label-v"
657 LABEL = "Verified"
658 VALUES = ("-1", "0", "1")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400659
660
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500661class ActionLabelCommitQueue(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600662 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500663
Alex Klein1699fab2022-09-08 08:46:06 -0600664 COMMAND = "label-cq"
665 LABEL = "Commit-Queue"
666 VALUES = ("0", "1", "2")
667
Mike Frysinger15b23e42014-12-05 17:00:05 -0500668
C Shapiro3f1f8242021-08-02 15:28:29 -0500669class ActionLabelOwnersOverride(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600670 """Change the Owners-Override label (1=Override)"""
C Shapiro3f1f8242021-08-02 15:28:29 -0500671
Alex Klein1699fab2022-09-08 08:46:06 -0600672 COMMAND = "label-oo"
673 LABEL = "Owners-Override"
674 VALUES = ("0", "1")
C Shapiro3f1f8242021-08-02 15:28:29 -0500675
Mike Frysinger15b23e42014-12-05 17:00:05 -0500676
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500677class _ActionSimpleParallelCLs(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600678 """Base helper for actions that only accept CLs."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500679
Alex Klein1699fab2022-09-08 08:46:06 -0600680 @staticmethod
681 def init_subparser(parser):
682 """Add arguments to this action's subparser."""
683 parser.add_argument(
684 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
685 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500686
Alex Klein1699fab2022-09-08 08:46:06 -0600687 def __call__(self, opts):
688 """Implement the action."""
689
690 def task(arg):
691 helper, cl = GetGerrit(opts, arg)
692 self._process_one(helper, cl, opts)
693
694 _run_parallel_tasks(task, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500695
696
697class ActionSubmit(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600698 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500699
Alex Klein1699fab2022-09-08 08:46:06 -0600700 COMMAND = "submit"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500701
Alex Klein1699fab2022-09-08 08:46:06 -0600702 @staticmethod
703 def _process_one(helper, cl, opts):
704 """Use |helper| to process the single |cl|."""
705 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400706
707
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500708class ActionAbandon(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600709 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500710
Alex Klein1699fab2022-09-08 08:46:06 -0600711 COMMAND = "abandon"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500712
Alex Klein1699fab2022-09-08 08:46:06 -0600713 @staticmethod
714 def init_subparser(parser):
715 """Add arguments to this action's subparser."""
716 parser.add_argument(
717 "-m",
718 "--msg",
719 "--message",
720 metavar="MESSAGE",
721 help="Include a message",
722 )
723 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger3af378b2021-03-12 01:34:04 -0500724
Alex Klein1699fab2022-09-08 08:46:06 -0600725 @staticmethod
726 def _process_one(helper, cl, opts):
727 """Use |helper| to process the single |cl|."""
728 helper.AbandonChange(
729 cl, msg=opts.msg, dryrun=opts.dryrun, notify=opts.notify
730 )
Mike Frysinger13f23a42013-05-13 17:32:01 -0400731
732
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500733class ActionRestore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600734 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500735
Alex Klein1699fab2022-09-08 08:46:06 -0600736 COMMAND = "restore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500737
Alex Klein1699fab2022-09-08 08:46:06 -0600738 @staticmethod
739 def _process_one(helper, cl, opts):
740 """Use |helper| to process the single |cl|."""
741 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400742
743
Tomasz Figa54d70992021-01-20 13:48:59 +0900744class ActionWorkInProgress(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600745 """Mark CLs as work in progress"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900746
Alex Klein1699fab2022-09-08 08:46:06 -0600747 COMMAND = "wip"
Tomasz Figa54d70992021-01-20 13:48:59 +0900748
Alex Klein1699fab2022-09-08 08:46:06 -0600749 @staticmethod
750 def _process_one(helper, cl, opts):
751 """Use |helper| to process the single |cl|."""
752 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900753
754
755class ActionReadyForReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600756 """Mark CLs as ready for review"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900757
Alex Klein1699fab2022-09-08 08:46:06 -0600758 COMMAND = "ready"
Tomasz Figa54d70992021-01-20 13:48:59 +0900759
Alex Klein1699fab2022-09-08 08:46:06 -0600760 @staticmethod
761 def _process_one(helper, cl, opts):
762 """Use |helper| to process the single |cl|."""
763 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900764
765
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500766class ActionReviewers(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600767 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700768
Alex Klein1699fab2022-09-08 08:46:06 -0600769 COMMAND = "reviewers"
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700770
Alex Klein1699fab2022-09-08 08:46:06 -0600771 @staticmethod
772 def init_subparser(parser):
773 """Add arguments to this action's subparser."""
774 parser.add_argument("cl", metavar="CL", help="The CL to update")
775 parser.add_argument(
776 "reviewers", nargs="+", help="The reviewers to add/remove"
777 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700778
Alex Klein1699fab2022-09-08 08:46:06 -0600779 @staticmethod
780 def __call__(opts):
781 """Implement the action."""
782 # Allow for optional leading '~'.
783 email_validator = re.compile(r"^[~]?%s$" % constants.EMAIL_REGEX)
784 add_list, remove_list, invalid_list = [], [], []
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500785
Alex Klein1699fab2022-09-08 08:46:06 -0600786 for email in opts.reviewers:
787 if not email_validator.match(email):
788 invalid_list.append(email)
789 elif email[0] == "~":
790 remove_list.append(email[1:])
791 else:
792 add_list.append(email)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500793
Alex Klein1699fab2022-09-08 08:46:06 -0600794 if invalid_list:
795 cros_build_lib.Die(
796 "Invalid email address(es): %s" % ", ".join(invalid_list)
797 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500798
Alex Klein1699fab2022-09-08 08:46:06 -0600799 if add_list or remove_list:
800 helper, cl = GetGerrit(opts, opts.cl)
801 helper.SetReviewers(
802 cl,
803 add=add_list,
804 remove=remove_list,
805 dryrun=opts.dryrun,
806 notify=opts.notify,
807 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700808
809
Brian Norrisd25af082021-10-29 11:25:31 -0700810class ActionAttentionSet(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600811 """Add/remove emails from the attention set (prepend with '~' to remove)"""
Brian Norrisd25af082021-10-29 11:25:31 -0700812
Alex Klein1699fab2022-09-08 08:46:06 -0600813 COMMAND = "attention"
Brian Norrisd25af082021-10-29 11:25:31 -0700814
Alex Klein1699fab2022-09-08 08:46:06 -0600815 @staticmethod
816 def init_subparser(parser):
817 """Add arguments to this action's subparser."""
818 parser.add_argument(
819 "-m",
820 "--msg",
821 "--message",
822 metavar="MESSAGE",
823 help="Optional message to include",
824 default="gerrit CLI",
825 )
826 parser.add_argument("cl", metavar="CL", help="The CL to update")
827 parser.add_argument(
828 "users",
829 nargs="+",
830 help="The users to add/remove from attention set",
831 )
Brian Norrisd25af082021-10-29 11:25:31 -0700832
Alex Klein1699fab2022-09-08 08:46:06 -0600833 @staticmethod
834 def __call__(opts):
835 """Implement the action."""
836 # Allow for optional leading '~'.
837 email_validator = re.compile(r"^[~]?%s$" % constants.EMAIL_REGEX)
838 add_list, remove_list, invalid_list = [], [], []
Brian Norrisd25af082021-10-29 11:25:31 -0700839
Alex Klein1699fab2022-09-08 08:46:06 -0600840 for email in opts.users:
841 if not email_validator.match(email):
842 invalid_list.append(email)
843 elif email[0] == "~":
844 remove_list.append(email[1:])
845 else:
846 add_list.append(email)
Brian Norrisd25af082021-10-29 11:25:31 -0700847
Alex Klein1699fab2022-09-08 08:46:06 -0600848 if invalid_list:
849 cros_build_lib.Die(
850 "Invalid email address(es): %s" % ", ".join(invalid_list)
851 )
Brian Norrisd25af082021-10-29 11:25:31 -0700852
Alex Klein1699fab2022-09-08 08:46:06 -0600853 if add_list or remove_list:
854 helper, cl = GetGerrit(opts, opts.cl)
855 helper.SetAttentionSet(
856 cl,
857 add=add_list,
858 remove=remove_list,
859 dryrun=opts.dryrun,
860 notify=opts.notify,
861 message=opts.msg,
862 )
Brian Norrisd25af082021-10-29 11:25:31 -0700863
864
Mike Frysinger62178ae2020-03-20 01:37:43 -0400865class ActionMessage(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600866 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500867
Alex Klein1699fab2022-09-08 08:46:06 -0600868 COMMAND = "message"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500869
Alex Klein1699fab2022-09-08 08:46:06 -0600870 @staticmethod
871 def init_subparser(parser):
872 """Add arguments to this action's subparser."""
873 _ActionSimpleParallelCLs.init_subparser(parser)
874 parser.add_argument("message", help="The message to post")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500875
Alex Klein1699fab2022-09-08 08:46:06 -0600876 @staticmethod
877 def _process_one(helper, cl, opts):
878 """Use |helper| to process the single |cl|."""
879 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530880
881
Mike Frysinger62178ae2020-03-20 01:37:43 -0400882class ActionTopic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600883 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500884
Alex Klein1699fab2022-09-08 08:46:06 -0600885 COMMAND = "topic"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500886
Alex Klein1699fab2022-09-08 08:46:06 -0600887 @staticmethod
888 def init_subparser(parser):
889 """Add arguments to this action's subparser."""
890 _ActionSimpleParallelCLs.init_subparser(parser)
891 parser.add_argument("topic", help="The topic to set")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500892
Alex Klein1699fab2022-09-08 08:46:06 -0600893 @staticmethod
894 def _process_one(helper, cl, opts):
895 """Use |helper| to process the single |cl|."""
896 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800897
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800898
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500899class ActionPrivate(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600900 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700901
Alex Klein1699fab2022-09-08 08:46:06 -0600902 COMMAND = "private"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500903
Alex Klein1699fab2022-09-08 08:46:06 -0600904 @staticmethod
905 def _process_one(helper, cl, opts):
906 """Use |helper| to process the single |cl|."""
907 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700908
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800909
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500910class ActionPublic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600911 """Mark CLs public"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500912
Alex Klein1699fab2022-09-08 08:46:06 -0600913 COMMAND = "public"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500914
Alex Klein1699fab2022-09-08 08:46:06 -0600915 @staticmethod
916 def _process_one(helper, cl, opts):
917 """Use |helper| to process the single |cl|."""
918 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500919
920
921class ActionSethashtags(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600922 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500923
Alex Klein1699fab2022-09-08 08:46:06 -0600924 COMMAND = "hashtags"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500925
Alex Klein1699fab2022-09-08 08:46:06 -0600926 @staticmethod
927 def init_subparser(parser):
928 """Add arguments to this action's subparser."""
929 parser.add_argument("cl", metavar="CL", help="The CL to update")
930 parser.add_argument(
931 "hashtags", nargs="+", help="The hashtags to add/remove"
932 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500933
Alex Klein1699fab2022-09-08 08:46:06 -0600934 @staticmethod
935 def __call__(opts):
936 """Implement the action."""
937 add = []
938 remove = []
939 for hashtag in opts.hashtags:
940 if hashtag.startswith("~"):
941 remove.append(hashtag[1:])
942 else:
943 add.append(hashtag)
944 helper, cl = GetGerrit(opts, opts.cl)
945 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800946
947
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500948class ActionDeletedraft(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600949 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500950
Alex Klein1699fab2022-09-08 08:46:06 -0600951 COMMAND = "deletedraft"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500952
Alex Klein1699fab2022-09-08 08:46:06 -0600953 @staticmethod
954 def _process_one(helper, cl, opts):
955 """Use |helper| to process the single |cl|."""
956 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800957
958
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500959class ActionReviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600960 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500961
Alex Klein1699fab2022-09-08 08:46:06 -0600962 COMMAND = "reviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500963
Alex Klein1699fab2022-09-08 08:46:06 -0600964 @staticmethod
965 def _process_one(helper, cl, opts):
966 """Use |helper| to process the single |cl|."""
967 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500968
969
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500970class ActionUnreviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600971 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500972
Alex Klein1699fab2022-09-08 08:46:06 -0600973 COMMAND = "unreviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500974
Alex Klein1699fab2022-09-08 08:46:06 -0600975 @staticmethod
976 def _process_one(helper, cl, opts):
977 """Use |helper| to process the single |cl|."""
978 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500979
980
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500981class ActionIgnore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600982 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500983
Alex Klein1699fab2022-09-08 08:46:06 -0600984 COMMAND = "ignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500985
Alex Klein1699fab2022-09-08 08:46:06 -0600986 @staticmethod
987 def _process_one(helper, cl, opts):
988 """Use |helper| to process the single |cl|."""
989 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500990
991
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500992class ActionUnignore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600993 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500994
Alex Klein1699fab2022-09-08 08:46:06 -0600995 COMMAND = "unignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500996
Alex Klein1699fab2022-09-08 08:46:06 -0600997 @staticmethod
998 def _process_one(helper, cl, opts):
999 """Use |helper| to process the single |cl|."""
1000 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -05001001
1002
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001003class ActionCherryPick(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001004 """Cherry-pick CLs to branches."""
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001005
Alex Klein1699fab2022-09-08 08:46:06 -06001006 COMMAND = "cherry-pick"
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001007
Alex Klein1699fab2022-09-08 08:46:06 -06001008 @staticmethod
1009 def init_subparser(parser):
1010 """Add arguments to this action's subparser."""
1011 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
1012 parser.add_argument(
1013 "--rev",
1014 "--revision",
1015 default="current",
1016 help="A specific revision or patchset",
1017 )
1018 parser.add_argument(
1019 "-m",
1020 "--msg",
1021 "--message",
1022 metavar="MESSAGE",
1023 help="Include a message",
1024 )
1025 parser.add_argument(
1026 "--branches",
1027 "--branch",
1028 "--br",
1029 action="split_extend",
1030 default=[],
1031 required=True,
1032 help="The destination branches",
1033 )
1034 parser.add_argument(
1035 "cls", nargs="+", metavar="CL", help="The CLs to cherry-pick"
1036 )
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001037
Alex Klein1699fab2022-09-08 08:46:06 -06001038 @staticmethod
1039 def __call__(opts):
1040 """Implement the action."""
1041 # Process branches in parallel, but CLs in serial in case of CL stacks.
1042 def task(branch):
1043 for arg in opts.cls:
1044 helper, cl = GetGerrit(opts, arg)
1045 ret = helper.CherryPick(
1046 cl,
1047 branch,
1048 rev=opts.rev,
1049 msg=opts.msg,
1050 dryrun=opts.dryrun,
1051 notify=opts.notify,
1052 )
1053 logging.debug("Response: %s", ret)
1054 if opts.format is OutputFormat.RAW:
1055 print(ret["_number"])
1056 else:
1057 uri = f'https://{helper.host}/c/{ret["_number"]}'
1058 print(uri_lib.ShortenUri(uri))
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001059
Alex Klein1699fab2022-09-08 08:46:06 -06001060 _run_parallel_tasks(task, *opts.branches)
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001061
1062
Mike Frysinger8037f752020-02-29 20:47:09 -05001063class ActionReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001064 """Review CLs with multiple settings
Mike Frysinger8037f752020-02-29 20:47:09 -05001065
Alex Klein1699fab2022-09-08 08:46:06 -06001066 The label option supports extended/multiple syntax for easy use. The --label
1067 option may be specified multiple times (as settings are merges), and multiple
1068 labels are allowed in a single argument. Each label has the form:
1069 <long or short name><=+-><value>
Mike Frysinger8037f752020-02-29 20:47:09 -05001070
Alex Klein1699fab2022-09-08 08:46:06 -06001071 Common arguments:
1072 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
1073 'V+1 CQ+2'
1074 'AS=1 V=1'
1075 """
Mike Frysinger8037f752020-02-29 20:47:09 -05001076
Alex Klein1699fab2022-09-08 08:46:06 -06001077 COMMAND = "review"
Mike Frysinger8037f752020-02-29 20:47:09 -05001078
Alex Klein1699fab2022-09-08 08:46:06 -06001079 class _SetLabel(argparse.Action):
1080 """Argparse action for setting labels."""
Mike Frysinger8037f752020-02-29 20:47:09 -05001081
Alex Klein1699fab2022-09-08 08:46:06 -06001082 LABEL_MAP = {
1083 "AS": "Auto-Submit",
1084 "CQ": "Commit-Queue",
1085 "CR": "Code-Review",
1086 "V": "Verified",
1087 }
Mike Frysinger8037f752020-02-29 20:47:09 -05001088
Alex Klein1699fab2022-09-08 08:46:06 -06001089 def __call__(self, parser, namespace, values, option_string=None):
1090 labels = getattr(namespace, self.dest)
1091 for request in values.split():
1092 if "=" in request:
1093 # Handle Verified=1 form.
1094 short, value = request.split("=", 1)
1095 elif "+" in request:
1096 # Handle Verified+1 form.
1097 short, value = request.split("+", 1)
1098 elif "-" in request:
1099 # Handle Verified-1 form.
1100 short, value = request.split("-", 1)
1101 value = "-%s" % (value,)
1102 else:
1103 parser.error(
1104 'Invalid label setting "%s". Must be Commit-Queue=1 or '
1105 "CQ+1 or CR-1." % (request,)
1106 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001107
Alex Klein1699fab2022-09-08 08:46:06 -06001108 # Convert possible short label names like "V" to "Verified".
1109 label = self.LABEL_MAP.get(short)
1110 if not label:
1111 label = short
Mike Frysinger8037f752020-02-29 20:47:09 -05001112
Alex Klein1699fab2022-09-08 08:46:06 -06001113 # We allow existing label requests to be overridden.
1114 labels[label] = value
Mike Frysinger8037f752020-02-29 20:47:09 -05001115
Alex Klein1699fab2022-09-08 08:46:06 -06001116 @classmethod
1117 def init_subparser(cls, parser):
1118 """Add arguments to this action's subparser."""
1119 parser.add_argument(
1120 "-m",
1121 "--msg",
1122 "--message",
1123 metavar="MESSAGE",
1124 help="Include a message",
1125 )
1126 parser.add_argument(
1127 "-l",
1128 "--label",
1129 dest="labels",
1130 action=cls._SetLabel,
1131 default={},
1132 help="Set a label with a value",
1133 )
1134 parser.add_argument(
1135 "--ready",
1136 default=None,
1137 action="store_true",
1138 help="Set CL status to ready-for-review",
1139 )
1140 parser.add_argument(
1141 "--wip",
1142 default=None,
1143 action="store_true",
1144 help="Set CL status to WIP",
1145 )
1146 parser.add_argument(
1147 "--reviewers",
1148 "--re",
1149 action="append",
1150 default=[],
1151 help="Add reviewers",
1152 )
1153 parser.add_argument(
1154 "--cc", action="append", default=[], help="Add people to CC"
1155 )
1156 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger8037f752020-02-29 20:47:09 -05001157
Alex Klein1699fab2022-09-08 08:46:06 -06001158 @staticmethod
1159 def _process_one(helper, cl, opts):
1160 """Use |helper| to process the single |cl|."""
1161 helper.SetReview(
1162 cl,
1163 msg=opts.msg,
1164 labels=opts.labels,
1165 dryrun=opts.dryrun,
1166 notify=opts.notify,
1167 reviewers=opts.reviewers,
1168 cc=opts.cc,
1169 ready=opts.ready,
1170 wip=opts.wip,
1171 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001172
1173
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001174class ActionAccount(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001175 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001176
Alex Klein1699fab2022-09-08 08:46:06 -06001177 COMMAND = "account"
1178 USE_PAGER = True
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001179
Alex Klein1699fab2022-09-08 08:46:06 -06001180 @staticmethod
1181 def init_subparser(parser):
1182 """Add arguments to this action's subparser."""
1183 parser.add_argument(
1184 "accounts",
1185 nargs="*",
1186 default=["self"],
1187 help="The accounts to query",
1188 )
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001189
Alex Klein1699fab2022-09-08 08:46:06 -06001190 @classmethod
1191 def __call__(cls, opts):
1192 """Implement the action."""
1193 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001194
Alex Klein1699fab2022-09-08 08:46:06 -06001195 def print_one(header, data):
1196 print(f"### {header}")
1197 compact = opts.format is OutputFormat.JSON
1198 print(pformat.json(data, compact=compact).rstrip())
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001199
Alex Klein1699fab2022-09-08 08:46:06 -06001200 def task(arg):
1201 detail = gob_util.FetchUrlJson(
1202 helper.host, f"accounts/{arg}/detail"
1203 )
1204 if not detail:
1205 print(f"{arg}: account not found")
1206 else:
1207 print_one("detail", detail)
1208 for field in (
1209 "groups",
1210 "capabilities",
1211 "preferences",
1212 "sshkeys",
1213 "gpgkeys",
1214 ):
1215 data = gob_util.FetchUrlJson(
1216 helper.host, f"accounts/{arg}/{field}"
1217 )
1218 print_one(field, data)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001219
Alex Klein1699fab2022-09-08 08:46:06 -06001220 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -08001221
1222
Mike Frysinger2295d792021-03-08 15:55:23 -05001223class ActionConfig(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001224 """Manage the gerrit tool's own config file
Mike Frysinger2295d792021-03-08 15:55:23 -05001225
Alex Klein1699fab2022-09-08 08:46:06 -06001226 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
1227 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
Mike Frysinger2295d792021-03-08 15:55:23 -05001228
Alex Klein1699fab2022-09-08 08:46:06 -06001229 # Set up subcommand aliases.
1230 [alias]
1231 common-search = search 'is:open project:something/i/care/about'
1232 """
Mike Frysinger2295d792021-03-08 15:55:23 -05001233
Alex Klein1699fab2022-09-08 08:46:06 -06001234 COMMAND = "config"
Mike Frysinger2295d792021-03-08 15:55:23 -05001235
Alex Klein1699fab2022-09-08 08:46:06 -06001236 @staticmethod
1237 def __call__(opts):
1238 """Implement the action."""
1239 # For now, this is a place holder for raising visibility for the config file
1240 # and its associated help text documentation.
1241 opts.parser.parse_args(["config", "--help"])
Mike Frysinger2295d792021-03-08 15:55:23 -05001242
1243
Mike Frysingere5450602021-03-08 15:34:17 -05001244class ActionHelp(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001245 """An alias to --help for CLI symmetry"""
Mike Frysingere5450602021-03-08 15:34:17 -05001246
Alex Klein1699fab2022-09-08 08:46:06 -06001247 COMMAND = "help"
1248 USE_PAGER = True
Mike Frysingere5450602021-03-08 15:34:17 -05001249
Alex Klein1699fab2022-09-08 08:46:06 -06001250 @staticmethod
1251 def init_subparser(parser):
1252 """Add arguments to this action's subparser."""
1253 parser.add_argument(
1254 "command", nargs="?", help="The command to display."
1255 )
Mike Frysingere5450602021-03-08 15:34:17 -05001256
Alex Klein1699fab2022-09-08 08:46:06 -06001257 @staticmethod
1258 def __call__(opts):
1259 """Implement the action."""
1260 # Show global help.
1261 if not opts.command:
1262 opts.parser.print_help()
1263 return
Mike Frysingere5450602021-03-08 15:34:17 -05001264
Alex Klein1699fab2022-09-08 08:46:06 -06001265 opts.parser.parse_args([opts.command, "--help"])
Mike Frysingere5450602021-03-08 15:34:17 -05001266
1267
Mike Frysinger484e2f82020-03-20 01:41:10 -04001268class ActionHelpAll(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001269 """Show all actions help output at once."""
Mike Frysinger484e2f82020-03-20 01:41:10 -04001270
Alex Klein1699fab2022-09-08 08:46:06 -06001271 COMMAND = "help-all"
1272 USE_PAGER = True
Mike Frysinger484e2f82020-03-20 01:41:10 -04001273
Alex Klein1699fab2022-09-08 08:46:06 -06001274 @staticmethod
1275 def __call__(opts):
1276 """Implement the action."""
1277 first = True
1278 for action in _GetActions():
1279 if first:
1280 first = False
1281 else:
1282 print("\n\n")
Mike Frysinger484e2f82020-03-20 01:41:10 -04001283
Alex Klein1699fab2022-09-08 08:46:06 -06001284 try:
1285 opts.parser.parse_args([action, "--help"])
1286 except SystemExit:
1287 pass
Mike Frysinger484e2f82020-03-20 01:41:10 -04001288
1289
Mike Frysinger65fc8632020-02-06 18:11:12 -05001290@memoize.Memoize
1291def _GetActions():
Alex Klein1699fab2022-09-08 08:46:06 -06001292 """Get all the possible actions we support.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001293
Alex Klein1699fab2022-09-08 08:46:06 -06001294 Returns:
1295 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1296 function that implements that command (e.g. UserActFoo).
1297 """
1298 VALID_NAME = re.compile(r"^[a-z][a-z-]*[a-z]$")
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001299
Alex Klein1699fab2022-09-08 08:46:06 -06001300 actions = {}
1301 for cls in globals().values():
1302 if (
1303 not inspect.isclass(cls)
1304 or not issubclass(cls, UserAction)
1305 or not getattr(cls, "COMMAND", None)
1306 ):
1307 continue
Mike Frysinger65fc8632020-02-06 18:11:12 -05001308
Alex Klein1699fab2022-09-08 08:46:06 -06001309 # Sanity check names for devs adding new commands. Should be quick.
1310 cmd = cls.COMMAND
1311 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1312 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001313
Alex Klein1699fab2022-09-08 08:46:06 -06001314 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001315
Alex Klein1699fab2022-09-08 08:46:06 -06001316 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001317
1318
Harry Cutts26076b32019-02-26 15:01:29 -08001319def _GetActionUsages():
Alex Klein1699fab2022-09-08 08:46:06 -06001320 """Formats a one-line usage and doc message for each action."""
1321 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001322
Alex Klein1699fab2022-09-08 08:46:06 -06001323 cmds = list(actions.keys())
1324 functions = list(actions.values())
1325 usages = [getattr(x, "usage", "") for x in functions]
1326 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001327
Alex Klein1699fab2022-09-08 08:46:06 -06001328 cmd_indent = len(max(cmds, key=len))
1329 usage_indent = len(max(usages, key=len))
1330 return "\n".join(
1331 " %-*s %-*s : %s" % (cmd_indent, cmd, usage_indent, usage, doc)
1332 for cmd, usage, doc in zip(cmds, usages, docs)
1333 )
Harry Cutts26076b32019-02-26 15:01:29 -08001334
1335
Mike Frysinger2295d792021-03-08 15:55:23 -05001336def _AddCommonOptions(parser, subparser):
Alex Klein1699fab2022-09-08 08:46:06 -06001337 """Add options that should work before & after the subcommand.
Mike Frysinger2295d792021-03-08 15:55:23 -05001338
Alex Klein1699fab2022-09-08 08:46:06 -06001339 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1340 """
1341 parser.add_common_argument_to_group(
1342 subparser,
1343 "--ne",
1344 "--no-emails",
1345 dest="notify",
1346 default="ALL",
1347 action="store_const",
1348 const="NONE",
1349 help="Do not send e-mail notifications",
1350 )
1351 parser.add_common_argument_to_group(
1352 subparser,
1353 "-n",
1354 "--dry-run",
1355 dest="dryrun",
1356 default=False,
1357 action="store_true",
1358 help="Show what would be done, but do not make changes",
1359 )
Mike Frysinger2295d792021-03-08 15:55:23 -05001360
1361
1362def GetBaseParser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001363 """Returns the common parser (i.e. no subparsers added)."""
1364 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001365There is no support for doing line-by-line code review via the command line.
1366This helps you manage various bits and CL status.
1367
Mike Frysingera1db2c42014-06-15 00:42:48 -07001368For general Gerrit documentation, see:
1369 https://gerrit-review.googlesource.com/Documentation/
1370The Searching Changes page covers the search query syntax:
1371 https://gerrit-review.googlesource.com/Documentation/user-search.html
1372
Mike Frysinger13f23a42013-05-13 17:32:01 -04001373Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001374 $ gerrit todo # List all the CLs that await your review.
1375 $ gerrit mine # List all of your open CLs.
1376 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1377 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1378 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001379 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
138028123.
1381 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1382CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001383Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001384 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1385with Commit-Queue=1.
1386 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1387CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001388 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001389
Harry Cutts26076b32019-02-26 15:01:29 -08001390Actions:
1391"""
Alex Klein1699fab2022-09-08 08:46:06 -06001392 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001393
Alex Klein1699fab2022-09-08 08:46:06 -06001394 site_params = config_lib.GetSiteParams()
1395 parser = commandline.ArgumentParser(
1396 description=description,
1397 default_log_level="notice",
1398 epilog="For subcommand help, use `gerrit help <command>`.",
1399 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001400
Alex Klein1699fab2022-09-08 08:46:06 -06001401 group = parser.add_argument_group("Server options")
1402 group.add_argument(
1403 "-i",
1404 "--internal",
1405 dest="gob",
1406 action="store_const",
1407 default=site_params.EXTERNAL_GOB_INSTANCE,
1408 const=site_params.INTERNAL_GOB_INSTANCE,
1409 help="Query internal Chrome Gerrit instance",
1410 )
1411 group.add_argument(
1412 "-g",
1413 "--gob",
1414 default=site_params.EXTERNAL_GOB_INSTANCE,
1415 help=("Gerrit (on borg) instance to query " "(default: %(default)s)"),
1416 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001417
Alex Klein1699fab2022-09-08 08:46:06 -06001418 group = parser.add_argument_group("CL options")
1419 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001420
Alex Klein1699fab2022-09-08 08:46:06 -06001421 group = parser.add_mutually_exclusive_group()
1422 parser.set_defaults(format=OutputFormat.AUTO)
1423 group.add_argument(
1424 "--format",
1425 action="enum",
1426 enum=OutputFormat,
1427 help="Output format to use.",
1428 )
1429 group.add_argument(
1430 "--raw",
1431 action="store_const",
1432 dest="format",
1433 const=OutputFormat.RAW,
1434 help="Alias for --format=raw.",
1435 )
1436 group.add_argument(
1437 "--json",
1438 action="store_const",
1439 dest="format",
1440 const=OutputFormat.JSON,
1441 help="Alias for --format=json.",
1442 )
Jack Rosenthal95aac172022-06-30 15:35:07 -06001443
Alex Klein1699fab2022-09-08 08:46:06 -06001444 group = parser.add_mutually_exclusive_group()
1445 group.add_argument(
1446 "--pager",
1447 action="store_true",
1448 default=sys.stdout.isatty(),
1449 help="Enable pager.",
1450 )
1451 group.add_argument(
1452 "--no-pager", action="store_false", dest="pager", help="Disable pager."
1453 )
1454 return parser
Mike Frysinger2295d792021-03-08 15:55:23 -05001455
1456
Alex Klein1699fab2022-09-08 08:46:06 -06001457def GetParser(
1458 parser: commandline.ArgumentParser = None,
1459) -> (commandline.ArgumentParser):
1460 """Returns the full parser to use for this module."""
1461 if parser is None:
1462 parser = GetBaseParser()
Mike Frysinger2295d792021-03-08 15:55:23 -05001463
Alex Klein1699fab2022-09-08 08:46:06 -06001464 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001465
Alex Klein1699fab2022-09-08 08:46:06 -06001466 # Subparsers are required by default under Python 2. Python 3 changed to
1467 # not required, but didn't include a required option until 3.7. Setting
1468 # the required member works in all versions (and setting dest name).
1469 subparsers = parser.add_subparsers(dest="action")
1470 subparsers.required = True
1471 for cmd, cls in actions.items():
1472 # Format the full docstring by removing the file level indentation.
1473 description = re.sub(r"^ ", "", cls.__doc__, flags=re.M)
1474 subparser = subparsers.add_parser(cmd, description=description)
1475 _AddCommonOptions(parser, subparser)
1476 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001477
Alex Klein1699fab2022-09-08 08:46:06 -06001478 return parser
Mike Frysinger108eda22018-06-06 18:45:12 -04001479
1480
Jack Rosenthal95aac172022-06-30 15:35:07 -06001481def start_pager():
Alex Klein1699fab2022-09-08 08:46:06 -06001482 """Re-spawn ourselves attached to a pager."""
1483 pager = os.environ.get("PAGER", "less")
1484 os.environ.setdefault("LESS", "FRX")
Jack Rosenthal95aac172022-06-30 15:35:07 -06001485 with subprocess.Popen(
Alex Klein1699fab2022-09-08 08:46:06 -06001486 # sys.argv can have some edge cases: we may not necessarily use
1487 # sys.executable if the script is executed as "python path/to/script".
1488 # If we upgrade to Python 3.10+, this should be changed to sys.orig_argv
1489 # for full accuracy.
1490 sys.argv,
1491 stdout=subprocess.PIPE,
1492 stderr=subprocess.STDOUT,
1493 env={"GERRIT_RESPAWN_FOR_PAGER": "1", **os.environ},
1494 ) as gerrit_proc:
1495 with subprocess.Popen(
1496 pager,
1497 shell=True,
1498 stdin=gerrit_proc.stdout,
1499 ) as pager_proc:
1500 # Send SIGINT to just the gerrit process, not the pager too.
1501 def _sighandler(signum, _frame):
1502 gerrit_proc.send_signal(signum)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001503
Alex Klein1699fab2022-09-08 08:46:06 -06001504 signal.signal(signal.SIGINT, _sighandler)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001505
Alex Klein1699fab2022-09-08 08:46:06 -06001506 pager_proc.communicate()
1507 # If the pager exits, and the gerrit process is still running, we
1508 # must terminate it.
1509 if gerrit_proc.poll() is None:
1510 gerrit_proc.terminate()
1511 sys.exit(gerrit_proc.wait())
Jack Rosenthal95aac172022-06-30 15:35:07 -06001512
1513
Mike Frysinger108eda22018-06-06 18:45:12 -04001514def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -06001515 base_parser = GetBaseParser()
1516 opts, subargs = base_parser.parse_known_args(argv)
Mike Frysinger2295d792021-03-08 15:55:23 -05001517
Alex Klein1699fab2022-09-08 08:46:06 -06001518 config = Config()
1519 if subargs:
1520 # If the action is an alias to an expanded value, we need to mutate the argv
1521 # and reparse things.
1522 action = config.expand_alias(subargs[0])
1523 if action != subargs[0]:
1524 pos = argv.index(subargs[0])
1525 argv = argv[:pos] + action + argv[pos + 1 :]
Mike Frysinger2295d792021-03-08 15:55:23 -05001526
Alex Klein1699fab2022-09-08 08:46:06 -06001527 parser = GetParser(parser=base_parser)
1528 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001529
Alex Klein1699fab2022-09-08 08:46:06 -06001530 # If we're running as a re-spawn for the pager, from this point on
1531 # we'll pretend we're attached to a TTY. This will give us colored
1532 # output when requested.
1533 if os.environ.pop("GERRIT_RESPAWN_FOR_PAGER", None) is not None:
1534 opts.pager = False
1535 sys.stdout.isatty = lambda: True
Jack Rosenthal95aac172022-06-30 15:35:07 -06001536
Alex Klein1699fab2022-09-08 08:46:06 -06001537 # In case the action wants to throw a parser error.
1538 opts.parser = parser
Mike Frysinger484e2f82020-03-20 01:41:10 -04001539
Alex Klein1699fab2022-09-08 08:46:06 -06001540 # A cache of gerrit helpers we'll load on demand.
1541 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001542
Alex Klein1699fab2022-09-08 08:46:06 -06001543 if opts.format is OutputFormat.AUTO:
1544 if sys.stdout.isatty():
1545 opts.format = OutputFormat.PRETTY
1546 else:
1547 opts.format = OutputFormat.RAW
Jack Rosenthale3a92672022-06-29 14:54:48 -06001548
Alex Klein1699fab2022-09-08 08:46:06 -06001549 opts.Freeze()
Mike Frysinger88f27292014-06-17 09:40:45 -07001550
Alex Klein1699fab2022-09-08 08:46:06 -06001551 # pylint: disable=global-statement
1552 global COLOR
1553 COLOR = terminal.Color(enabled=opts.color)
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001554
Alex Klein1699fab2022-09-08 08:46:06 -06001555 # Now look up the requested user action and run it.
1556 actions = _GetActions()
1557 action_class = actions[opts.action]
1558 if action_class.USE_PAGER and opts.pager:
1559 start_pager()
1560 obj = action_class()
1561 try:
1562 obj(opts)
1563 except (
1564 cros_build_lib.RunCommandError,
1565 gerrit.GerritException,
1566 gob_util.GOBError,
1567 ) as e:
1568 cros_build_lib.Die(e)