blob: f087d056d1270da7de2924403a3482d729067504 [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
Alex Klein4507b172023-01-13 11:39:51 -0700464 children: a function which takes a node and returns the adjacent nodes
Alex Klein1699fab2022-09-08 08:46:06 -0600465 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 Klein4507b172023-01-13 11:39:51 -0700484 """List CLs matching a query, and 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 Klein4507b172023-01-13 11:39:51 -0700531 # Handle empty results. If we found a commit that was pushed
532 # directly (e.g. a bot commit), then gerrit won't know about it.
Alex Klein1699fab2022-09-08 08:46:06 -0600533 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 Klein4507b172023-01-13 11:39:51 -0700542 # Our query might have matched more than one result. This can come
543 # up when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id
544 # shows up across multiple repos/branches. We blindly check all of
545 # them in the hopes that all open ones are what the user wants, but
546 # then again the CQ-DEPEND syntax itself is unable to differentiate.
547 # *shrug*
Alex Klein1699fab2022-09-08 08:46:06 -0600548 if len(changes) > 1:
549 logging.warning(
550 "CL %s has an ambiguous CQ dependency %s",
551 cl,
552 dep.ToGerritQueryText(),
553 )
554 for change in changes:
555 if change.status == "NEW":
556 yield change
Mike Frysinger5726da92017-09-20 22:14:25 -0400557
Alex Klein1699fab2022-09-08 08:46:06 -0600558 @classmethod
559 def _Children(cls, opts, querier, cl):
560 """Yields the Gerrit dependencies of a patch"""
561 for change in cls._ProcessDeps(
562 opts, querier, cl, cl.GerritDependencies(), False
563 ):
564 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700565
Paul Hobbs89765232015-06-24 14:07:49 -0700566
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500567class ActionInspect(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600568 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500569
Alex Klein1699fab2022-09-08 08:46:06 -0600570 COMMAND = "inspect"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500571
Alex Klein1699fab2022-09-08 08:46:06 -0600572 @staticmethod
573 def init_subparser(parser):
574 """Add arguments to this action's subparser."""
575 _ActionSearchQuery.init_subparser(parser)
576 parser.add_argument(
577 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
578 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500579
Alex Klein1699fab2022-09-08 08:46:06 -0600580 @staticmethod
581 def __call__(opts):
582 """Implement the action."""
583 cls = []
584 for arg in opts.cls:
585 helper, cl = GetGerrit(opts, arg)
586 change = FilteredQuery(opts, "change:%s" % cl, helper=helper)
587 if change:
588 cls.extend(change)
589 else:
590 logging.warning("no results found for CL %s", arg)
591 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400592
593
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500594class _ActionLabeler(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600595 """Base helper for setting labels."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500596
Alex Klein1699fab2022-09-08 08:46:06 -0600597 LABEL = None
598 VALUES = None
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500599
Alex Klein1699fab2022-09-08 08:46:06 -0600600 @classmethod
601 def init_subparser(cls, parser):
602 """Add arguments to this action's subparser."""
603 parser.add_argument(
604 "-m",
605 "--msg",
606 "--message",
607 metavar="MESSAGE",
608 help="Optional message to include",
609 )
610 parser.add_argument(
611 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
612 )
613 parser.add_argument(
614 "value",
615 nargs=1,
616 metavar="value",
617 choices=cls.VALUES,
618 help="The label value; one of [%(choices)s]",
619 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500620
Alex Klein1699fab2022-09-08 08:46:06 -0600621 @classmethod
622 def __call__(cls, opts):
623 """Implement the action."""
624 # Convert user-friendly command line option into a gerrit parameter.
625 def task(arg):
626 helper, cl = GetGerrit(opts, arg)
627 helper.SetReview(
628 cl,
629 labels={cls.LABEL: opts.value[0]},
630 msg=opts.msg,
631 dryrun=opts.dryrun,
632 notify=opts.notify,
633 )
634
635 _run_parallel_tasks(task, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500636
637
638class ActionLabelAutoSubmit(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600639 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500640
Alex Klein1699fab2022-09-08 08:46:06 -0600641 COMMAND = "label-as"
642 LABEL = "Auto-Submit"
643 VALUES = ("0", "1")
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600644
645
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500646class ActionLabelCodeReview(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600647 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500648
Alex Klein1699fab2022-09-08 08:46:06 -0600649 COMMAND = "label-cr"
650 LABEL = "Code-Review"
651 VALUES = ("-2", "-1", "0", "1", "2")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400652
653
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500654class ActionLabelVerified(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600655 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500656
Alex Klein1699fab2022-09-08 08:46:06 -0600657 COMMAND = "label-v"
658 LABEL = "Verified"
659 VALUES = ("-1", "0", "1")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400660
661
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500662class ActionLabelCommitQueue(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600663 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500664
Alex Klein1699fab2022-09-08 08:46:06 -0600665 COMMAND = "label-cq"
666 LABEL = "Commit-Queue"
667 VALUES = ("0", "1", "2")
668
Mike Frysinger15b23e42014-12-05 17:00:05 -0500669
C Shapiro3f1f8242021-08-02 15:28:29 -0500670class ActionLabelOwnersOverride(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600671 """Change the Owners-Override label (1=Override)"""
C Shapiro3f1f8242021-08-02 15:28:29 -0500672
Alex Klein1699fab2022-09-08 08:46:06 -0600673 COMMAND = "label-oo"
674 LABEL = "Owners-Override"
675 VALUES = ("0", "1")
C Shapiro3f1f8242021-08-02 15:28:29 -0500676
Mike Frysinger15b23e42014-12-05 17:00:05 -0500677
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500678class _ActionSimpleParallelCLs(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600679 """Base helper for actions that only accept CLs."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500680
Alex Klein1699fab2022-09-08 08:46:06 -0600681 @staticmethod
682 def init_subparser(parser):
683 """Add arguments to this action's subparser."""
684 parser.add_argument(
685 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
686 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500687
Alex Klein1699fab2022-09-08 08:46:06 -0600688 def __call__(self, opts):
689 """Implement the action."""
690
691 def task(arg):
692 helper, cl = GetGerrit(opts, arg)
693 self._process_one(helper, cl, opts)
694
695 _run_parallel_tasks(task, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500696
697
698class ActionSubmit(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600699 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500700
Alex Klein1699fab2022-09-08 08:46:06 -0600701 COMMAND = "submit"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500702
Alex Klein1699fab2022-09-08 08:46:06 -0600703 @staticmethod
704 def _process_one(helper, cl, opts):
705 """Use |helper| to process the single |cl|."""
706 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400707
708
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500709class ActionAbandon(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600710 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500711
Alex Klein1699fab2022-09-08 08:46:06 -0600712 COMMAND = "abandon"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500713
Alex Klein1699fab2022-09-08 08:46:06 -0600714 @staticmethod
715 def init_subparser(parser):
716 """Add arguments to this action's subparser."""
717 parser.add_argument(
718 "-m",
719 "--msg",
720 "--message",
721 metavar="MESSAGE",
722 help="Include a message",
723 )
724 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger3af378b2021-03-12 01:34:04 -0500725
Alex Klein1699fab2022-09-08 08:46:06 -0600726 @staticmethod
727 def _process_one(helper, cl, opts):
728 """Use |helper| to process the single |cl|."""
729 helper.AbandonChange(
730 cl, msg=opts.msg, dryrun=opts.dryrun, notify=opts.notify
731 )
Mike Frysinger13f23a42013-05-13 17:32:01 -0400732
733
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500734class ActionRestore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600735 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500736
Alex Klein1699fab2022-09-08 08:46:06 -0600737 COMMAND = "restore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500738
Alex Klein1699fab2022-09-08 08:46:06 -0600739 @staticmethod
740 def _process_one(helper, cl, opts):
741 """Use |helper| to process the single |cl|."""
742 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400743
744
Tomasz Figa54d70992021-01-20 13:48:59 +0900745class ActionWorkInProgress(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600746 """Mark CLs as work in progress"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900747
Alex Klein1699fab2022-09-08 08:46:06 -0600748 COMMAND = "wip"
Tomasz Figa54d70992021-01-20 13:48:59 +0900749
Alex Klein1699fab2022-09-08 08:46:06 -0600750 @staticmethod
751 def _process_one(helper, cl, opts):
752 """Use |helper| to process the single |cl|."""
753 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900754
755
756class ActionReadyForReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600757 """Mark CLs as ready for review"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900758
Alex Klein1699fab2022-09-08 08:46:06 -0600759 COMMAND = "ready"
Tomasz Figa54d70992021-01-20 13:48:59 +0900760
Alex Klein1699fab2022-09-08 08:46:06 -0600761 @staticmethod
762 def _process_one(helper, cl, opts):
763 """Use |helper| to process the single |cl|."""
764 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900765
766
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500767class ActionReviewers(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600768 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700769
Alex Klein1699fab2022-09-08 08:46:06 -0600770 COMMAND = "reviewers"
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700771
Alex Klein1699fab2022-09-08 08:46:06 -0600772 @staticmethod
773 def init_subparser(parser):
774 """Add arguments to this action's subparser."""
775 parser.add_argument("cl", metavar="CL", help="The CL to update")
776 parser.add_argument(
777 "reviewers", nargs="+", help="The reviewers to add/remove"
778 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700779
Alex Klein1699fab2022-09-08 08:46:06 -0600780 @staticmethod
781 def __call__(opts):
782 """Implement the action."""
783 # Allow for optional leading '~'.
784 email_validator = re.compile(r"^[~]?%s$" % constants.EMAIL_REGEX)
785 add_list, remove_list, invalid_list = [], [], []
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500786
Alex Klein1699fab2022-09-08 08:46:06 -0600787 for email in opts.reviewers:
788 if not email_validator.match(email):
789 invalid_list.append(email)
790 elif email[0] == "~":
791 remove_list.append(email[1:])
792 else:
793 add_list.append(email)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500794
Alex Klein1699fab2022-09-08 08:46:06 -0600795 if invalid_list:
796 cros_build_lib.Die(
797 "Invalid email address(es): %s" % ", ".join(invalid_list)
798 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500799
Alex Klein1699fab2022-09-08 08:46:06 -0600800 if add_list or remove_list:
801 helper, cl = GetGerrit(opts, opts.cl)
802 helper.SetReviewers(
803 cl,
804 add=add_list,
805 remove=remove_list,
806 dryrun=opts.dryrun,
807 notify=opts.notify,
808 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700809
810
Brian Norrisd25af082021-10-29 11:25:31 -0700811class ActionAttentionSet(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600812 """Add/remove emails from the attention set (prepend with '~' to remove)"""
Brian Norrisd25af082021-10-29 11:25:31 -0700813
Alex Klein1699fab2022-09-08 08:46:06 -0600814 COMMAND = "attention"
Brian Norrisd25af082021-10-29 11:25:31 -0700815
Alex Klein1699fab2022-09-08 08:46:06 -0600816 @staticmethod
817 def init_subparser(parser):
818 """Add arguments to this action's subparser."""
819 parser.add_argument(
820 "-m",
821 "--msg",
822 "--message",
823 metavar="MESSAGE",
824 help="Optional message to include",
825 default="gerrit CLI",
826 )
827 parser.add_argument("cl", metavar="CL", help="The CL to update")
828 parser.add_argument(
829 "users",
830 nargs="+",
831 help="The users to add/remove from attention set",
832 )
Brian Norrisd25af082021-10-29 11:25:31 -0700833
Alex Klein1699fab2022-09-08 08:46:06 -0600834 @staticmethod
835 def __call__(opts):
836 """Implement the action."""
837 # Allow for optional leading '~'.
838 email_validator = re.compile(r"^[~]?%s$" % constants.EMAIL_REGEX)
839 add_list, remove_list, invalid_list = [], [], []
Brian Norrisd25af082021-10-29 11:25:31 -0700840
Alex Klein1699fab2022-09-08 08:46:06 -0600841 for email in opts.users:
842 if not email_validator.match(email):
843 invalid_list.append(email)
844 elif email[0] == "~":
845 remove_list.append(email[1:])
846 else:
847 add_list.append(email)
Brian Norrisd25af082021-10-29 11:25:31 -0700848
Alex Klein1699fab2022-09-08 08:46:06 -0600849 if invalid_list:
850 cros_build_lib.Die(
851 "Invalid email address(es): %s" % ", ".join(invalid_list)
852 )
Brian Norrisd25af082021-10-29 11:25:31 -0700853
Alex Klein1699fab2022-09-08 08:46:06 -0600854 if add_list or remove_list:
855 helper, cl = GetGerrit(opts, opts.cl)
856 helper.SetAttentionSet(
857 cl,
858 add=add_list,
859 remove=remove_list,
860 dryrun=opts.dryrun,
861 notify=opts.notify,
862 message=opts.msg,
863 )
Brian Norrisd25af082021-10-29 11:25:31 -0700864
865
Mike Frysinger62178ae2020-03-20 01:37:43 -0400866class ActionMessage(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600867 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500868
Alex Klein1699fab2022-09-08 08:46:06 -0600869 COMMAND = "message"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500870
Alex Klein1699fab2022-09-08 08:46:06 -0600871 @staticmethod
872 def init_subparser(parser):
873 """Add arguments to this action's subparser."""
874 _ActionSimpleParallelCLs.init_subparser(parser)
875 parser.add_argument("message", help="The message to post")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500876
Alex Klein1699fab2022-09-08 08:46:06 -0600877 @staticmethod
878 def _process_one(helper, cl, opts):
879 """Use |helper| to process the single |cl|."""
880 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530881
882
Mike Frysinger62178ae2020-03-20 01:37:43 -0400883class ActionTopic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600884 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500885
Alex Klein1699fab2022-09-08 08:46:06 -0600886 COMMAND = "topic"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500887
Alex Klein1699fab2022-09-08 08:46:06 -0600888 @staticmethod
889 def init_subparser(parser):
890 """Add arguments to this action's subparser."""
891 _ActionSimpleParallelCLs.init_subparser(parser)
892 parser.add_argument("topic", help="The topic to set")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500893
Alex Klein1699fab2022-09-08 08:46:06 -0600894 @staticmethod
895 def _process_one(helper, cl, opts):
896 """Use |helper| to process the single |cl|."""
897 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800898
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800899
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500900class ActionPrivate(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600901 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700902
Alex Klein1699fab2022-09-08 08:46:06 -0600903 COMMAND = "private"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500904
Alex Klein1699fab2022-09-08 08:46:06 -0600905 @staticmethod
906 def _process_one(helper, cl, opts):
907 """Use |helper| to process the single |cl|."""
908 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700909
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800910
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500911class ActionPublic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600912 """Mark CLs public"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500913
Alex Klein1699fab2022-09-08 08:46:06 -0600914 COMMAND = "public"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500915
Alex Klein1699fab2022-09-08 08:46:06 -0600916 @staticmethod
917 def _process_one(helper, cl, opts):
918 """Use |helper| to process the single |cl|."""
919 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500920
921
922class ActionSethashtags(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600923 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500924
Alex Klein1699fab2022-09-08 08:46:06 -0600925 COMMAND = "hashtags"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500926
Alex Klein1699fab2022-09-08 08:46:06 -0600927 @staticmethod
928 def init_subparser(parser):
929 """Add arguments to this action's subparser."""
930 parser.add_argument("cl", metavar="CL", help="The CL to update")
931 parser.add_argument(
932 "hashtags", nargs="+", help="The hashtags to add/remove"
933 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500934
Alex Klein1699fab2022-09-08 08:46:06 -0600935 @staticmethod
936 def __call__(opts):
937 """Implement the action."""
938 add = []
939 remove = []
940 for hashtag in opts.hashtags:
941 if hashtag.startswith("~"):
942 remove.append(hashtag[1:])
943 else:
944 add.append(hashtag)
945 helper, cl = GetGerrit(opts, opts.cl)
946 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800947
948
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500949class ActionDeletedraft(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600950 """Delete draft CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500951
Alex Klein1699fab2022-09-08 08:46:06 -0600952 COMMAND = "deletedraft"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500953
Alex Klein1699fab2022-09-08 08:46:06 -0600954 @staticmethod
955 def _process_one(helper, cl, opts):
956 """Use |helper| to process the single |cl|."""
957 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800958
959
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500960class ActionReviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600961 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500962
Alex Klein1699fab2022-09-08 08:46:06 -0600963 COMMAND = "reviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500964
Alex Klein1699fab2022-09-08 08:46:06 -0600965 @staticmethod
966 def _process_one(helper, cl, opts):
967 """Use |helper| to process the single |cl|."""
968 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500969
970
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500971class ActionUnreviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600972 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500973
Alex Klein1699fab2022-09-08 08:46:06 -0600974 COMMAND = "unreviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500975
Alex Klein1699fab2022-09-08 08:46:06 -0600976 @staticmethod
977 def _process_one(helper, cl, opts):
978 """Use |helper| to process the single |cl|."""
979 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500980
981
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500982class ActionIgnore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600983 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500984
Alex Klein1699fab2022-09-08 08:46:06 -0600985 COMMAND = "ignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500986
Alex Klein1699fab2022-09-08 08:46:06 -0600987 @staticmethod
988 def _process_one(helper, cl, opts):
989 """Use |helper| to process the single |cl|."""
990 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500991
992
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500993class ActionUnignore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600994 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500995
Alex Klein1699fab2022-09-08 08:46:06 -0600996 COMMAND = "unignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500997
Alex Klein1699fab2022-09-08 08:46:06 -0600998 @staticmethod
999 def _process_one(helper, cl, opts):
1000 """Use |helper| to process the single |cl|."""
1001 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -05001002
1003
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001004class ActionCherryPick(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001005 """Cherry-pick CLs to branches."""
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001006
Alex Klein1699fab2022-09-08 08:46:06 -06001007 COMMAND = "cherry-pick"
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001008
Alex Klein1699fab2022-09-08 08:46:06 -06001009 @staticmethod
1010 def init_subparser(parser):
1011 """Add arguments to this action's subparser."""
1012 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
1013 parser.add_argument(
1014 "--rev",
1015 "--revision",
1016 default="current",
1017 help="A specific revision or patchset",
1018 )
1019 parser.add_argument(
1020 "-m",
1021 "--msg",
1022 "--message",
1023 metavar="MESSAGE",
1024 help="Include a message",
1025 )
1026 parser.add_argument(
1027 "--branches",
1028 "--branch",
1029 "--br",
1030 action="split_extend",
1031 default=[],
1032 required=True,
1033 help="The destination branches",
1034 )
1035 parser.add_argument(
1036 "cls", nargs="+", metavar="CL", help="The CLs to cherry-pick"
1037 )
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001038
Alex Klein1699fab2022-09-08 08:46:06 -06001039 @staticmethod
1040 def __call__(opts):
1041 """Implement the action."""
1042 # Process branches in parallel, but CLs in serial in case of CL stacks.
1043 def task(branch):
1044 for arg in opts.cls:
1045 helper, cl = GetGerrit(opts, arg)
1046 ret = helper.CherryPick(
1047 cl,
1048 branch,
1049 rev=opts.rev,
1050 msg=opts.msg,
1051 dryrun=opts.dryrun,
1052 notify=opts.notify,
1053 )
1054 logging.debug("Response: %s", ret)
1055 if opts.format is OutputFormat.RAW:
1056 print(ret["_number"])
1057 else:
1058 uri = f'https://{helper.host}/c/{ret["_number"]}'
1059 print(uri_lib.ShortenUri(uri))
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001060
Alex Klein1699fab2022-09-08 08:46:06 -06001061 _run_parallel_tasks(task, *opts.branches)
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001062
1063
Mike Frysinger8037f752020-02-29 20:47:09 -05001064class ActionReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001065 """Review CLs with multiple settings
Mike Frysinger8037f752020-02-29 20:47:09 -05001066
Alex Klein4507b172023-01-13 11:39:51 -07001067 The label option supports extended/multiple syntax for easy use. The --label
1068 option may be specified multiple times (as settings are merges), and
1069 multiple labels are allowed in a single argument. Each label has the form:
Alex Klein1699fab2022-09-08 08:46:06 -06001070 <long or short name><=+-><value>
Mike Frysinger8037f752020-02-29 20:47:09 -05001071
Alex Klein1699fab2022-09-08 08:46:06 -06001072 Common arguments:
1073 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
1074 'V+1 CQ+2'
1075 'AS=1 V=1'
1076 """
Mike Frysinger8037f752020-02-29 20:47:09 -05001077
Alex Klein1699fab2022-09-08 08:46:06 -06001078 COMMAND = "review"
Mike Frysinger8037f752020-02-29 20:47:09 -05001079
Alex Klein1699fab2022-09-08 08:46:06 -06001080 class _SetLabel(argparse.Action):
1081 """Argparse action for setting labels."""
Mike Frysinger8037f752020-02-29 20:47:09 -05001082
Alex Klein1699fab2022-09-08 08:46:06 -06001083 LABEL_MAP = {
1084 "AS": "Auto-Submit",
1085 "CQ": "Commit-Queue",
1086 "CR": "Code-Review",
1087 "V": "Verified",
1088 }
Mike Frysinger8037f752020-02-29 20:47:09 -05001089
Alex Klein1699fab2022-09-08 08:46:06 -06001090 def __call__(self, parser, namespace, values, option_string=None):
1091 labels = getattr(namespace, self.dest)
1092 for request in values.split():
1093 if "=" in request:
1094 # Handle Verified=1 form.
1095 short, value = request.split("=", 1)
1096 elif "+" in request:
1097 # Handle Verified+1 form.
1098 short, value = request.split("+", 1)
1099 elif "-" in request:
1100 # Handle Verified-1 form.
1101 short, value = request.split("-", 1)
1102 value = "-%s" % (value,)
1103 else:
1104 parser.error(
1105 'Invalid label setting "%s". Must be Commit-Queue=1 or '
1106 "CQ+1 or CR-1." % (request,)
1107 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001108
Alex Klein1699fab2022-09-08 08:46:06 -06001109 # Convert possible short label names like "V" to "Verified".
1110 label = self.LABEL_MAP.get(short)
1111 if not label:
1112 label = short
Mike Frysinger8037f752020-02-29 20:47:09 -05001113
Alex Klein1699fab2022-09-08 08:46:06 -06001114 # We allow existing label requests to be overridden.
1115 labels[label] = value
Mike Frysinger8037f752020-02-29 20:47:09 -05001116
Alex Klein1699fab2022-09-08 08:46:06 -06001117 @classmethod
1118 def init_subparser(cls, parser):
1119 """Add arguments to this action's subparser."""
1120 parser.add_argument(
1121 "-m",
1122 "--msg",
1123 "--message",
1124 metavar="MESSAGE",
1125 help="Include a message",
1126 )
1127 parser.add_argument(
1128 "-l",
1129 "--label",
1130 dest="labels",
1131 action=cls._SetLabel,
1132 default={},
1133 help="Set a label with a value",
1134 )
1135 parser.add_argument(
1136 "--ready",
1137 default=None,
1138 action="store_true",
1139 help="Set CL status to ready-for-review",
1140 )
1141 parser.add_argument(
1142 "--wip",
1143 default=None,
1144 action="store_true",
1145 help="Set CL status to WIP",
1146 )
1147 parser.add_argument(
1148 "--reviewers",
1149 "--re",
1150 action="append",
1151 default=[],
1152 help="Add reviewers",
1153 )
1154 parser.add_argument(
1155 "--cc", action="append", default=[], help="Add people to CC"
1156 )
1157 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger8037f752020-02-29 20:47:09 -05001158
Alex Klein1699fab2022-09-08 08:46:06 -06001159 @staticmethod
1160 def _process_one(helper, cl, opts):
1161 """Use |helper| to process the single |cl|."""
1162 helper.SetReview(
1163 cl,
1164 msg=opts.msg,
1165 labels=opts.labels,
1166 dryrun=opts.dryrun,
1167 notify=opts.notify,
1168 reviewers=opts.reviewers,
1169 cc=opts.cc,
1170 ready=opts.ready,
1171 wip=opts.wip,
1172 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001173
1174
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001175class ActionAccount(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001176 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001177
Alex Klein1699fab2022-09-08 08:46:06 -06001178 COMMAND = "account"
1179 USE_PAGER = True
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001180
Alex Klein1699fab2022-09-08 08:46:06 -06001181 @staticmethod
1182 def init_subparser(parser):
1183 """Add arguments to this action's subparser."""
1184 parser.add_argument(
1185 "accounts",
1186 nargs="*",
1187 default=["self"],
1188 help="The accounts to query",
1189 )
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001190
Alex Klein1699fab2022-09-08 08:46:06 -06001191 @classmethod
1192 def __call__(cls, opts):
1193 """Implement the action."""
1194 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001195
Alex Klein1699fab2022-09-08 08:46:06 -06001196 def print_one(header, data):
1197 print(f"### {header}")
1198 compact = opts.format is OutputFormat.JSON
1199 print(pformat.json(data, compact=compact).rstrip())
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001200
Alex Klein1699fab2022-09-08 08:46:06 -06001201 def task(arg):
1202 detail = gob_util.FetchUrlJson(
1203 helper.host, f"accounts/{arg}/detail"
1204 )
1205 if not detail:
1206 print(f"{arg}: account not found")
1207 else:
1208 print_one("detail", detail)
1209 for field in (
1210 "groups",
1211 "capabilities",
1212 "preferences",
1213 "sshkeys",
1214 "gpgkeys",
1215 ):
1216 data = gob_util.FetchUrlJson(
1217 helper.host, f"accounts/{arg}/{field}"
1218 )
1219 print_one(field, data)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001220
Alex Klein1699fab2022-09-08 08:46:06 -06001221 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -08001222
1223
Mike Frysinger2295d792021-03-08 15:55:23 -05001224class ActionConfig(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001225 """Manage the gerrit tool's own config file
Mike Frysinger2295d792021-03-08 15:55:23 -05001226
Alex Klein1699fab2022-09-08 08:46:06 -06001227 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
1228 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
Mike Frysinger2295d792021-03-08 15:55:23 -05001229
Alex Klein1699fab2022-09-08 08:46:06 -06001230 # Set up subcommand aliases.
1231 [alias]
1232 common-search = search 'is:open project:something/i/care/about'
1233 """
Mike Frysinger2295d792021-03-08 15:55:23 -05001234
Alex Klein1699fab2022-09-08 08:46:06 -06001235 COMMAND = "config"
Mike Frysinger2295d792021-03-08 15:55:23 -05001236
Alex Klein1699fab2022-09-08 08:46:06 -06001237 @staticmethod
1238 def __call__(opts):
1239 """Implement the action."""
Alex Klein4507b172023-01-13 11:39:51 -07001240 # For now, this is a place holder for raising visibility for the config
1241 # file and its associated help text documentation.
Alex Klein1699fab2022-09-08 08:46:06 -06001242 opts.parser.parse_args(["config", "--help"])
Mike Frysinger2295d792021-03-08 15:55:23 -05001243
1244
Mike Frysingere5450602021-03-08 15:34:17 -05001245class ActionHelp(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001246 """An alias to --help for CLI symmetry"""
Mike Frysingere5450602021-03-08 15:34:17 -05001247
Alex Klein1699fab2022-09-08 08:46:06 -06001248 COMMAND = "help"
1249 USE_PAGER = True
Mike Frysingere5450602021-03-08 15:34:17 -05001250
Alex Klein1699fab2022-09-08 08:46:06 -06001251 @staticmethod
1252 def init_subparser(parser):
1253 """Add arguments to this action's subparser."""
1254 parser.add_argument(
1255 "command", nargs="?", help="The command to display."
1256 )
Mike Frysingere5450602021-03-08 15:34:17 -05001257
Alex Klein1699fab2022-09-08 08:46:06 -06001258 @staticmethod
1259 def __call__(opts):
1260 """Implement the action."""
1261 # Show global help.
1262 if not opts.command:
1263 opts.parser.print_help()
1264 return
Mike Frysingere5450602021-03-08 15:34:17 -05001265
Alex Klein1699fab2022-09-08 08:46:06 -06001266 opts.parser.parse_args([opts.command, "--help"])
Mike Frysingere5450602021-03-08 15:34:17 -05001267
1268
Mike Frysinger484e2f82020-03-20 01:41:10 -04001269class ActionHelpAll(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001270 """Show all actions help output at once."""
Mike Frysinger484e2f82020-03-20 01:41:10 -04001271
Alex Klein1699fab2022-09-08 08:46:06 -06001272 COMMAND = "help-all"
1273 USE_PAGER = True
Mike Frysinger484e2f82020-03-20 01:41:10 -04001274
Alex Klein1699fab2022-09-08 08:46:06 -06001275 @staticmethod
1276 def __call__(opts):
1277 """Implement the action."""
1278 first = True
1279 for action in _GetActions():
1280 if first:
1281 first = False
1282 else:
1283 print("\n\n")
Mike Frysinger484e2f82020-03-20 01:41:10 -04001284
Alex Klein1699fab2022-09-08 08:46:06 -06001285 try:
1286 opts.parser.parse_args([action, "--help"])
1287 except SystemExit:
1288 pass
Mike Frysinger484e2f82020-03-20 01:41:10 -04001289
1290
Mike Frysinger65fc8632020-02-06 18:11:12 -05001291@memoize.Memoize
1292def _GetActions():
Alex Klein1699fab2022-09-08 08:46:06 -06001293 """Get all the possible actions we support.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001294
Alex Klein1699fab2022-09-08 08:46:06 -06001295 Returns:
1296 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1297 function that implements that command (e.g. UserActFoo).
1298 """
1299 VALID_NAME = re.compile(r"^[a-z][a-z-]*[a-z]$")
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001300
Alex Klein1699fab2022-09-08 08:46:06 -06001301 actions = {}
1302 for cls in globals().values():
1303 if (
1304 not inspect.isclass(cls)
1305 or not issubclass(cls, UserAction)
1306 or not getattr(cls, "COMMAND", None)
1307 ):
1308 continue
Mike Frysinger65fc8632020-02-06 18:11:12 -05001309
Alex Klein1699fab2022-09-08 08:46:06 -06001310 # Sanity check names for devs adding new commands. Should be quick.
1311 cmd = cls.COMMAND
1312 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1313 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001314
Alex Klein1699fab2022-09-08 08:46:06 -06001315 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001316
Alex Klein1699fab2022-09-08 08:46:06 -06001317 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001318
1319
Harry Cutts26076b32019-02-26 15:01:29 -08001320def _GetActionUsages():
Alex Klein1699fab2022-09-08 08:46:06 -06001321 """Formats a one-line usage and doc message for each action."""
1322 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001323
Alex Klein1699fab2022-09-08 08:46:06 -06001324 cmds = list(actions.keys())
1325 functions = list(actions.values())
1326 usages = [getattr(x, "usage", "") for x in functions]
1327 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001328
Alex Klein1699fab2022-09-08 08:46:06 -06001329 cmd_indent = len(max(cmds, key=len))
1330 usage_indent = len(max(usages, key=len))
1331 return "\n".join(
1332 " %-*s %-*s : %s" % (cmd_indent, cmd, usage_indent, usage, doc)
1333 for cmd, usage, doc in zip(cmds, usages, docs)
1334 )
Harry Cutts26076b32019-02-26 15:01:29 -08001335
1336
Mike Frysinger2295d792021-03-08 15:55:23 -05001337def _AddCommonOptions(parser, subparser):
Alex Klein1699fab2022-09-08 08:46:06 -06001338 """Add options that should work before & after the subcommand.
Mike Frysinger2295d792021-03-08 15:55:23 -05001339
Alex Klein1699fab2022-09-08 08:46:06 -06001340 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1341 """
1342 parser.add_common_argument_to_group(
1343 subparser,
1344 "--ne",
1345 "--no-emails",
1346 dest="notify",
1347 default="ALL",
1348 action="store_const",
1349 const="NONE",
1350 help="Do not send e-mail notifications",
1351 )
1352 parser.add_common_argument_to_group(
1353 subparser,
1354 "-n",
1355 "--dry-run",
1356 dest="dryrun",
1357 default=False,
1358 action="store_true",
1359 help="Show what would be done, but do not make changes",
1360 )
Mike Frysinger2295d792021-03-08 15:55:23 -05001361
1362
1363def GetBaseParser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001364 """Returns the common parser (i.e. no subparsers added)."""
1365 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001366There is no support for doing line-by-line code review via the command line.
1367This helps you manage various bits and CL status.
1368
Mike Frysingera1db2c42014-06-15 00:42:48 -07001369For general Gerrit documentation, see:
1370 https://gerrit-review.googlesource.com/Documentation/
1371The Searching Changes page covers the search query syntax:
1372 https://gerrit-review.googlesource.com/Documentation/user-search.html
1373
Mike Frysinger13f23a42013-05-13 17:32:01 -04001374Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001375 $ gerrit todo # List all the CLs that await your review.
1376 $ gerrit mine # List all of your open CLs.
1377 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1378 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1379 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001380 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
138128123.
1382 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1383CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001384Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001385 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1386with Commit-Queue=1.
1387 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1388CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001389 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001390
Harry Cutts26076b32019-02-26 15:01:29 -08001391Actions:
1392"""
Alex Klein1699fab2022-09-08 08:46:06 -06001393 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001394
Alex Klein1699fab2022-09-08 08:46:06 -06001395 site_params = config_lib.GetSiteParams()
1396 parser = commandline.ArgumentParser(
1397 description=description,
1398 default_log_level="notice",
1399 epilog="For subcommand help, use `gerrit help <command>`.",
1400 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001401
Alex Klein1699fab2022-09-08 08:46:06 -06001402 group = parser.add_argument_group("Server options")
1403 group.add_argument(
1404 "-i",
1405 "--internal",
1406 dest="gob",
1407 action="store_const",
1408 default=site_params.EXTERNAL_GOB_INSTANCE,
1409 const=site_params.INTERNAL_GOB_INSTANCE,
1410 help="Query internal Chrome Gerrit instance",
1411 )
1412 group.add_argument(
1413 "-g",
1414 "--gob",
1415 default=site_params.EXTERNAL_GOB_INSTANCE,
1416 help=("Gerrit (on borg) instance to query " "(default: %(default)s)"),
1417 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001418
Alex Klein1699fab2022-09-08 08:46:06 -06001419 group = parser.add_argument_group("CL options")
1420 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001421
Alex Klein1699fab2022-09-08 08:46:06 -06001422 group = parser.add_mutually_exclusive_group()
1423 parser.set_defaults(format=OutputFormat.AUTO)
1424 group.add_argument(
1425 "--format",
1426 action="enum",
1427 enum=OutputFormat,
1428 help="Output format to use.",
1429 )
1430 group.add_argument(
1431 "--raw",
1432 action="store_const",
1433 dest="format",
1434 const=OutputFormat.RAW,
1435 help="Alias for --format=raw.",
1436 )
1437 group.add_argument(
1438 "--json",
1439 action="store_const",
1440 dest="format",
1441 const=OutputFormat.JSON,
1442 help="Alias for --format=json.",
1443 )
Jack Rosenthal95aac172022-06-30 15:35:07 -06001444
Alex Klein1699fab2022-09-08 08:46:06 -06001445 group = parser.add_mutually_exclusive_group()
1446 group.add_argument(
1447 "--pager",
1448 action="store_true",
1449 default=sys.stdout.isatty(),
1450 help="Enable pager.",
1451 )
1452 group.add_argument(
1453 "--no-pager", action="store_false", dest="pager", help="Disable pager."
1454 )
1455 return parser
Mike Frysinger2295d792021-03-08 15:55:23 -05001456
1457
Alex Klein1699fab2022-09-08 08:46:06 -06001458def GetParser(
1459 parser: commandline.ArgumentParser = None,
1460) -> (commandline.ArgumentParser):
1461 """Returns the full parser to use for this module."""
1462 if parser is None:
1463 parser = GetBaseParser()
Mike Frysinger2295d792021-03-08 15:55:23 -05001464
Alex Klein1699fab2022-09-08 08:46:06 -06001465 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001466
Alex Klein1699fab2022-09-08 08:46:06 -06001467 # Subparsers are required by default under Python 2. Python 3 changed to
1468 # not required, but didn't include a required option until 3.7. Setting
1469 # the required member works in all versions (and setting dest name).
1470 subparsers = parser.add_subparsers(dest="action")
1471 subparsers.required = True
1472 for cmd, cls in actions.items():
1473 # Format the full docstring by removing the file level indentation.
1474 description = re.sub(r"^ ", "", cls.__doc__, flags=re.M)
1475 subparser = subparsers.add_parser(cmd, description=description)
1476 _AddCommonOptions(parser, subparser)
1477 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001478
Alex Klein1699fab2022-09-08 08:46:06 -06001479 return parser
Mike Frysinger108eda22018-06-06 18:45:12 -04001480
1481
Jack Rosenthal95aac172022-06-30 15:35:07 -06001482def start_pager():
Alex Klein1699fab2022-09-08 08:46:06 -06001483 """Re-spawn ourselves attached to a pager."""
1484 pager = os.environ.get("PAGER", "less")
1485 os.environ.setdefault("LESS", "FRX")
Jack Rosenthal95aac172022-06-30 15:35:07 -06001486 with subprocess.Popen(
Alex Klein1699fab2022-09-08 08:46:06 -06001487 # sys.argv can have some edge cases: we may not necessarily use
1488 # sys.executable if the script is executed as "python path/to/script".
1489 # If we upgrade to Python 3.10+, this should be changed to sys.orig_argv
1490 # for full accuracy.
1491 sys.argv,
1492 stdout=subprocess.PIPE,
1493 stderr=subprocess.STDOUT,
1494 env={"GERRIT_RESPAWN_FOR_PAGER": "1", **os.environ},
1495 ) as gerrit_proc:
1496 with subprocess.Popen(
1497 pager,
1498 shell=True,
1499 stdin=gerrit_proc.stdout,
1500 ) as pager_proc:
1501 # Send SIGINT to just the gerrit process, not the pager too.
1502 def _sighandler(signum, _frame):
1503 gerrit_proc.send_signal(signum)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001504
Alex Klein1699fab2022-09-08 08:46:06 -06001505 signal.signal(signal.SIGINT, _sighandler)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001506
Alex Klein1699fab2022-09-08 08:46:06 -06001507 pager_proc.communicate()
1508 # If the pager exits, and the gerrit process is still running, we
1509 # must terminate it.
1510 if gerrit_proc.poll() is None:
1511 gerrit_proc.terminate()
1512 sys.exit(gerrit_proc.wait())
Jack Rosenthal95aac172022-06-30 15:35:07 -06001513
1514
Mike Frysinger108eda22018-06-06 18:45:12 -04001515def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -06001516 base_parser = GetBaseParser()
1517 opts, subargs = base_parser.parse_known_args(argv)
Mike Frysinger2295d792021-03-08 15:55:23 -05001518
Alex Klein1699fab2022-09-08 08:46:06 -06001519 config = Config()
1520 if subargs:
Alex Klein4507b172023-01-13 11:39:51 -07001521 # If the action is an alias to an expanded value, we need to mutate the
1522 # argv and reparse things.
Alex Klein1699fab2022-09-08 08:46:06 -06001523 action = config.expand_alias(subargs[0])
1524 if action != subargs[0]:
1525 pos = argv.index(subargs[0])
1526 argv = argv[:pos] + action + argv[pos + 1 :]
Mike Frysinger2295d792021-03-08 15:55:23 -05001527
Alex Klein1699fab2022-09-08 08:46:06 -06001528 parser = GetParser(parser=base_parser)
1529 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001530
Alex Klein1699fab2022-09-08 08:46:06 -06001531 # If we're running as a re-spawn for the pager, from this point on
1532 # we'll pretend we're attached to a TTY. This will give us colored
1533 # output when requested.
1534 if os.environ.pop("GERRIT_RESPAWN_FOR_PAGER", None) is not None:
1535 opts.pager = False
1536 sys.stdout.isatty = lambda: True
Jack Rosenthal95aac172022-06-30 15:35:07 -06001537
Alex Klein1699fab2022-09-08 08:46:06 -06001538 # In case the action wants to throw a parser error.
1539 opts.parser = parser
Mike Frysinger484e2f82020-03-20 01:41:10 -04001540
Alex Klein1699fab2022-09-08 08:46:06 -06001541 # A cache of gerrit helpers we'll load on demand.
1542 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001543
Alex Klein1699fab2022-09-08 08:46:06 -06001544 if opts.format is OutputFormat.AUTO:
1545 if sys.stdout.isatty():
1546 opts.format = OutputFormat.PRETTY
1547 else:
1548 opts.format = OutputFormat.RAW
Jack Rosenthale3a92672022-06-29 14:54:48 -06001549
Alex Klein1699fab2022-09-08 08:46:06 -06001550 opts.Freeze()
Mike Frysinger88f27292014-06-17 09:40:45 -07001551
Alex Klein1699fab2022-09-08 08:46:06 -06001552 # pylint: disable=global-statement
1553 global COLOR
1554 COLOR = terminal.Color(enabled=opts.color)
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001555
Alex Klein1699fab2022-09-08 08:46:06 -06001556 # Now look up the requested user action and run it.
1557 actions = _GetActions()
1558 action_class = actions[opts.action]
1559 if action_class.USE_PAGER and opts.pager:
1560 start_pager()
1561 obj = action_class()
1562 try:
1563 obj(opts)
1564 except (
1565 cros_build_lib.RunCommandError,
1566 gerrit.GerritException,
1567 gob_util.GOBError,
1568 ) as e:
1569 cros_build_lib.Die(e)