blob: 1a37ef8c6d526d12c0c2d4f4142697f82356ccc2 [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."""
Mike Frysinger16474792023-03-01 01:18:00 -0500167
Alex Klein1699fab2022-09-08 08:46:06 -0600168 # When we run in parallel, we can hit the max requests limit.
169 def check_exc(e):
170 if not isinstance(e, gob_util.GOBError):
171 raise e
172 return e.http_status == 429
Mike Frysingera9751c92021-04-30 10:12:37 -0400173
Alex Klein1699fab2022-09-08 08:46:06 -0600174 @retry_util.WithRetry(5, handler=check_exc, sleep=1, backoff_factor=2)
175 def retry(*args):
176 try:
177 task(*args)
178 except gob_util.GOBError as e:
179 if e.http_status != 429:
180 logging.warning("%s: skipping due: %s", args, e)
181 else:
182 raise
Mike Frysingera9751c92021-04-30 10:12:37 -0400183
Alex Klein1699fab2022-09-08 08:46:06 -0600184 with parallel.BackgroundTaskRunner(retry, processes=CONNECTION_LIMIT) as q:
185 for arg in args:
186 q.put([arg])
Mike Frysinger254f33f2019-12-11 13:54:29 -0500187
188
Mike Frysinger13f23a42013-05-13 17:32:01 -0400189def limits(cls):
Alex Klein1699fab2022-09-08 08:46:06 -0600190 """Given a dict of fields, calculate the longest string lengths
Mike Frysinger13f23a42013-05-13 17:32:01 -0400191
Alex Klein1699fab2022-09-08 08:46:06 -0600192 This allows you to easily format the output of many results so that the
193 various cols all line up correctly.
194 """
195 lims = {}
196 for cl in cls:
197 for k in cl.keys():
198 # Use %s rather than str() to avoid codec issues.
199 # We also do this so we can format integers.
200 lims[k] = max(lims.get(k, 0), len("%s" % cl[k]))
201 return lims
Mike Frysinger13f23a42013-05-13 17:32:01 -0400202
203
Mike Frysinger88f27292014-06-17 09:40:45 -0700204# TODO: This func really needs to be merged into the core gerrit logic.
205def GetGerrit(opts, cl=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600206 """Auto pick the right gerrit instance based on the |cl|
Mike Frysinger88f27292014-06-17 09:40:45 -0700207
Alex Klein1699fab2022-09-08 08:46:06 -0600208 Args:
209 opts: The general options object.
210 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
Mike Frysinger88f27292014-06-17 09:40:45 -0700211
Alex Klein1699fab2022-09-08 08:46:06 -0600212 Returns:
213 A tuple of a gerrit object and a sanitized CL #.
214 """
215 gob = opts.gob
216 if cl is not None:
217 if cl.startswith("*") or cl.startswith("chrome-internal:"):
218 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
219 if cl.startswith("*"):
220 cl = cl[1:]
221 else:
222 cl = cl[16:]
223 elif ":" in cl:
224 gob, cl = cl.split(":", 1)
Mike Frysinger88f27292014-06-17 09:40:45 -0700225
Alex Klein1699fab2022-09-08 08:46:06 -0600226 if not gob in opts.gerrit:
227 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
Mike Frysinger88f27292014-06-17 09:40:45 -0700228
Alex Klein1699fab2022-09-08 08:46:06 -0600229 return (opts.gerrit[gob], cl)
Mike Frysinger88f27292014-06-17 09:40:45 -0700230
231
Mike Frysinger13f23a42013-05-13 17:32:01 -0400232def GetApprovalSummary(_opts, cls):
Alex Klein1699fab2022-09-08 08:46:06 -0600233 """Return a dict of the most important approvals"""
Alex Kleine37b8762023-04-17 12:16:15 -0600234 approvs = {x: "" for x in GERRIT_SUMMARY_CATS}
Alex Klein1699fab2022-09-08 08:46:06 -0600235 for approver in cls.get("currentPatchSet", {}).get("approvals", []):
236 cats = GERRIT_APPROVAL_MAP.get(approver["type"])
237 if not cats:
238 logging.warning(
239 "unknown gerrit approval type: %s", approver["type"]
240 )
241 continue
242 cat = cats[0].strip()
243 val = int(approver["value"])
244 if not cat in approvs:
245 # Ignore the extended categories in the summary view.
246 continue
247 elif approvs[cat] == "":
248 approvs[cat] = val
249 elif val < 0:
250 approvs[cat] = min(approvs[cat], val)
251 else:
252 approvs[cat] = max(approvs[cat], val)
253 return approvs
Mike Frysinger13f23a42013-05-13 17:32:01 -0400254
255
Mike Frysingera1b4b272017-04-05 16:11:00 -0400256def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600257 """Pretty print a single result"""
258 if lims is None:
259 lims = {"url": 0, "project": 0}
Mike Frysinger13f23a42013-05-13 17:32:01 -0400260
Alex Klein1699fab2022-09-08 08:46:06 -0600261 status = ""
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400262
Alex Klein1699fab2022-09-08 08:46:06 -0600263 if opts.verbose:
264 status += "%s " % (cl["status"],)
265 else:
266 status += "%s " % (GERRIT_SUMMARY_MAP.get(cl["status"], cl["status"]),)
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400267
Alex Klein1699fab2022-09-08 08:46:06 -0600268 if show_approvals and not opts.verbose:
269 approvs = GetApprovalSummary(opts, cl)
270 for cat in GERRIT_SUMMARY_CATS:
271 if approvs[cat] in ("", 0):
272 functor = lambda x: x
273 elif approvs[cat] < 0:
274 functor = red
275 else:
276 functor = green
277 status += functor("%s:%2s " % (cat, approvs[cat]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400278
Alex Klein1699fab2022-09-08 08:46:06 -0600279 if opts.format is OutputFormat.MARKDOWN:
280 print("* %s - %s" % (uri_lib.ShortenUri(cl["url"]), cl["subject"]))
281 else:
282 print(
283 "%s %s%-*s %s"
284 % (
285 blue("%-*s" % (lims["url"], cl["url"])),
286 status,
287 lims["project"],
288 cl["project"],
289 cl["subject"],
290 )
291 )
Mike Frysinger13f23a42013-05-13 17:32:01 -0400292
Alex Klein1699fab2022-09-08 08:46:06 -0600293 if show_approvals and opts.verbose:
294 for approver in cl["currentPatchSet"].get("approvals", []):
295 functor = red if int(approver["value"]) < 0 else green
296 n = functor("%2s" % approver["value"])
297 t = GERRIT_APPROVAL_MAP.get(
298 approver["type"], [approver["type"], approver["type"]]
299 )[1]
300 print(" %s %s %s" % (n, t, approver["by"]["email"]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400301
302
Mike Frysingera1b4b272017-04-05 16:11:00 -0400303def PrintCls(opts, cls, lims=None, show_approvals=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600304 """Print all results based on the requested format."""
305 if opts.format is OutputFormat.RAW:
306 site_params = config_lib.GetSiteParams()
307 pfx = ""
308 # Special case internal Chrome GoB as that is what most devs use.
309 # They can always redirect the list elsewhere via the -g option.
310 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
311 pfx = site_params.INTERNAL_CHANGE_PREFIX
312 for cl in cls:
313 print("%s%s" % (pfx, cl["number"]))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400314
Alex Klein1699fab2022-09-08 08:46:06 -0600315 elif opts.format is OutputFormat.JSON:
316 json.dump(cls, sys.stdout)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400317
Alex Klein1699fab2022-09-08 08:46:06 -0600318 else:
319 if lims is None:
320 lims = limits(cls)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400321
Alex Klein1699fab2022-09-08 08:46:06 -0600322 for cl in cls:
323 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400324
325
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400326def _Query(opts, query, raw=True, helper=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600327 """Queries Gerrit with a query string built from the commandline options"""
328 if opts.branch is not None:
329 query += " branch:%s" % opts.branch
330 if opts.project is not None:
331 query += " project: %s" % opts.project
332 if opts.topic is not None:
333 query += " topic: %s" % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800334
Alex Klein1699fab2022-09-08 08:46:06 -0600335 if helper is None:
336 helper, _ = GetGerrit(opts)
337 return helper.Query(query, raw=raw, bypass_cache=False)
Paul Hobbs89765232015-06-24 14:07:49 -0700338
339
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400340def FilteredQuery(opts, query, helper=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600341 """Query gerrit and filter/clean up the results"""
342 ret = []
Paul Hobbs89765232015-06-24 14:07:49 -0700343
Alex Klein1699fab2022-09-08 08:46:06 -0600344 logging.debug("Running query: %s", query)
345 for cl in _Query(opts, query, raw=True, helper=helper):
346 # Gerrit likes to return a stats record too.
347 if not "project" in cl:
348 continue
Mike Frysinger13f23a42013-05-13 17:32:01 -0400349
Alex Klein1699fab2022-09-08 08:46:06 -0600350 # Strip off common leading names since the result is still
351 # unique over the whole tree.
352 if not opts.verbose:
353 for pfx in (
354 "aosp",
355 "chromeos",
356 "chromiumos",
357 "external",
358 "overlays",
359 "platform",
360 "third_party",
361 ):
362 if cl["project"].startswith("%s/" % pfx):
363 cl["project"] = cl["project"][len(pfx) + 1 :]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400364
Alex Klein1699fab2022-09-08 08:46:06 -0600365 cl["url"] = uri_lib.ShortenUri(cl["url"])
Mike Frysinger479f1192017-09-14 22:36:30 -0400366
Alex Klein1699fab2022-09-08 08:46:06 -0600367 ret.append(cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400368
Alex Klein1699fab2022-09-08 08:46:06 -0600369 if opts.sort == "unsorted":
370 return ret
371 if opts.sort == "number":
372 key = lambda x: int(x[opts.sort])
373 else:
374 key = lambda x: x[opts.sort]
375 return sorted(ret, key=key)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400376
377
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500378class _ActionSearchQuery(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600379 """Base class for actions that perform searches."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500380
Alex Klein1699fab2022-09-08 08:46:06 -0600381 USE_PAGER = True
Jack Rosenthal95aac172022-06-30 15:35:07 -0600382
Alex Klein1699fab2022-09-08 08:46:06 -0600383 @staticmethod
384 def init_subparser(parser):
385 """Add arguments to this action's subparser."""
386 parser.add_argument(
387 "--sort",
388 default="number",
389 help='Key to sort on (number, project); use "unsorted" '
390 "to disable",
391 )
392 parser.add_argument(
393 "-b", "--branch", help="Limit output to the specific branch"
394 )
395 parser.add_argument(
396 "-p", "--project", help="Limit output to the specific project"
397 )
398 parser.add_argument(
399 "-t", "--topic", help="Limit output to the specific topic"
400 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500401
402
403class ActionTodo(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600404 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500405
Alex Klein1699fab2022-09-08 08:46:06 -0600406 COMMAND = "todo"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500407
Alex Klein1699fab2022-09-08 08:46:06 -0600408 @staticmethod
409 def __call__(opts):
410 """Implement the action."""
411 cls = FilteredQuery(opts, "attention:self")
412 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400413
414
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500415class ActionSearch(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600416 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500417
Alex Klein1699fab2022-09-08 08:46:06 -0600418 COMMAND = "search"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500419
Alex Klein1699fab2022-09-08 08:46:06 -0600420 @staticmethod
421 def init_subparser(parser):
422 """Add arguments to this action's subparser."""
423 _ActionSearchQuery.init_subparser(parser)
424 parser.add_argument("query", help="The search query")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500425
Alex Klein1699fab2022-09-08 08:46:06 -0600426 @staticmethod
427 def __call__(opts):
428 """Implement the action."""
429 cls = FilteredQuery(opts, opts.query)
430 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400431
432
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500433class ActionMine(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600434 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500435
Alex Klein1699fab2022-09-08 08:46:06 -0600436 COMMAND = "mine"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500437
Alex Klein1699fab2022-09-08 08:46:06 -0600438 @staticmethod
439 def init_subparser(parser):
440 """Add arguments to this action's subparser."""
441 _ActionSearchQuery.init_subparser(parser)
442 parser.add_argument(
443 "--draft",
444 default=False,
445 action="store_true",
446 help="Show draft changes",
447 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500448
Alex Klein1699fab2022-09-08 08:46:06 -0600449 @staticmethod
450 def __call__(opts):
451 """Implement the action."""
452 if opts.draft:
453 rule = "is:draft"
454 else:
455 rule = "status:new"
456 cls = FilteredQuery(opts, "owner:self %s" % (rule,))
457 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700458
459
Paul Hobbs89765232015-06-24 14:07:49 -0700460def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
Alex Klein1699fab2022-09-08 08:46:06 -0600461 """Runs breadth first search starting from the nodes in |to_visit|
Paul Hobbs89765232015-06-24 14:07:49 -0700462
Alex Klein1699fab2022-09-08 08:46:06 -0600463 Args:
464 to_visit: the starting nodes
Alex Klein4507b172023-01-13 11:39:51 -0700465 children: a function which takes a node and returns the adjacent nodes
Alex Klein1699fab2022-09-08 08:46:06 -0600466 visited_key: a function for deduplicating node visits. Defaults to the
467 identity function (lambda x: x)
Paul Hobbs89765232015-06-24 14:07:49 -0700468
Alex Klein1699fab2022-09-08 08:46:06 -0600469 Returns:
470 A list of nodes which are reachable from any node in |to_visit| by calling
471 |children| any number of times.
472 """
473 to_visit = list(to_visit)
474 seen = set(visited_key(x) for x in to_visit)
475 for node in to_visit:
476 for child in children(node):
477 key = visited_key(child)
478 if key not in seen:
479 seen.add(key)
480 to_visit.append(child)
481 return to_visit
Paul Hobbs89765232015-06-24 14:07:49 -0700482
483
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500484class ActionDeps(_ActionSearchQuery):
Alex Klein4507b172023-01-13 11:39:51 -0700485 """List CLs matching a query, and transitive dependencies of those CLs."""
Paul Hobbs89765232015-06-24 14:07:49 -0700486
Alex Klein1699fab2022-09-08 08:46:06 -0600487 COMMAND = "deps"
Paul Hobbs89765232015-06-24 14:07:49 -0700488
Alex Klein1699fab2022-09-08 08:46:06 -0600489 @staticmethod
490 def init_subparser(parser):
491 """Add arguments to this action's subparser."""
492 _ActionSearchQuery.init_subparser(parser)
493 parser.add_argument("query", help="The search query")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500494
Alex Klein1699fab2022-09-08 08:46:06 -0600495 def __call__(self, opts):
496 """Implement the action."""
497 cls = _Query(opts, opts.query, raw=False)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500498
Alex Klein1699fab2022-09-08 08:46:06 -0600499 @memoize.Memoize
500 def _QueryChange(cl, helper=None):
501 return _Query(opts, cl, raw=False, helper=helper)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500502
Alex Klein1699fab2022-09-08 08:46:06 -0600503 transitives = _BreadthFirstSearch(
504 cls,
505 functools.partial(self._Children, opts, _QueryChange),
506 visited_key=lambda cl: cl.PatchLink(),
507 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500508
Alex Klein1699fab2022-09-08 08:46:06 -0600509 # This is a hack to avoid losing GoB host for each CL. The PrintCls
510 # function assumes the GoB host specified by the user is the only one
511 # that is ever used, but the deps command walks across hosts.
512 if opts.format is OutputFormat.RAW:
513 print("\n".join(x.PatchLink() for x in transitives))
514 else:
515 transitives_raw = [cl.patch_dict for cl in transitives]
516 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500517
Alex Klein1699fab2022-09-08 08:46:06 -0600518 @staticmethod
519 def _ProcessDeps(opts, querier, cl, deps, required):
520 """Yields matching dependencies for a patch"""
521 # We need to query the change to guarantee that we have a .gerrit_number
522 for dep in deps:
523 if not dep.remote in opts.gerrit:
524 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
525 remote=dep.remote, print_cmd=opts.debug
526 )
527 helper = opts.gerrit[dep.remote]
Mike Frysingerb3300c42017-07-20 01:41:17 -0400528
Alex Klein1699fab2022-09-08 08:46:06 -0600529 # TODO(phobbs) this should maybe catch network errors.
530 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400531
Alex Klein4507b172023-01-13 11:39:51 -0700532 # Handle empty results. If we found a commit that was pushed
533 # directly (e.g. a bot commit), then gerrit won't know about it.
Alex Klein1699fab2022-09-08 08:46:06 -0600534 if not changes:
535 if required:
536 logging.error(
537 "CL %s depends on %s which cannot be found",
538 cl,
539 dep.ToGerritQueryText(),
540 )
541 continue
Mike Frysinger5726da92017-09-20 22:14:25 -0400542
Alex Klein4507b172023-01-13 11:39:51 -0700543 # Our query might have matched more than one result. This can come
544 # up when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id
545 # shows up across multiple repos/branches. We blindly check all of
546 # them in the hopes that all open ones are what the user wants, but
547 # then again the CQ-DEPEND syntax itself is unable to differentiate.
548 # *shrug*
Alex Klein1699fab2022-09-08 08:46:06 -0600549 if len(changes) > 1:
550 logging.warning(
551 "CL %s has an ambiguous CQ dependency %s",
552 cl,
553 dep.ToGerritQueryText(),
554 )
555 for change in changes:
556 if change.status == "NEW":
557 yield change
Mike Frysinger5726da92017-09-20 22:14:25 -0400558
Alex Klein1699fab2022-09-08 08:46:06 -0600559 @classmethod
560 def _Children(cls, opts, querier, cl):
561 """Yields the Gerrit dependencies of a patch"""
562 for change in cls._ProcessDeps(
563 opts, querier, cl, cl.GerritDependencies(), False
564 ):
565 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700566
Paul Hobbs89765232015-06-24 14:07:49 -0700567
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500568class ActionInspect(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600569 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500570
Alex Klein1699fab2022-09-08 08:46:06 -0600571 COMMAND = "inspect"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500572
Alex Klein1699fab2022-09-08 08:46:06 -0600573 @staticmethod
574 def init_subparser(parser):
575 """Add arguments to this action's subparser."""
576 _ActionSearchQuery.init_subparser(parser)
577 parser.add_argument(
578 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
579 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500580
Alex Klein1699fab2022-09-08 08:46:06 -0600581 @staticmethod
582 def __call__(opts):
583 """Implement the action."""
584 cls = []
585 for arg in opts.cls:
586 helper, cl = GetGerrit(opts, arg)
587 change = FilteredQuery(opts, "change:%s" % cl, helper=helper)
588 if change:
589 cls.extend(change)
590 else:
591 logging.warning("no results found for CL %s", arg)
592 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400593
594
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500595class _ActionLabeler(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600596 """Base helper for setting labels."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500597
Alex Klein1699fab2022-09-08 08:46:06 -0600598 LABEL = None
599 VALUES = None
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500600
Alex Klein1699fab2022-09-08 08:46:06 -0600601 @classmethod
602 def init_subparser(cls, parser):
603 """Add arguments to this action's subparser."""
604 parser.add_argument(
605 "-m",
606 "--msg",
607 "--message",
608 metavar="MESSAGE",
609 help="Optional message to include",
610 )
611 parser.add_argument(
612 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
613 )
614 parser.add_argument(
615 "value",
616 nargs=1,
617 metavar="value",
618 choices=cls.VALUES,
619 help="The label value; one of [%(choices)s]",
620 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500621
Alex Klein1699fab2022-09-08 08:46:06 -0600622 @classmethod
623 def __call__(cls, opts):
624 """Implement the action."""
Mike Frysinger16474792023-03-01 01:18:00 -0500625
Alex Klein1699fab2022-09-08 08:46:06 -0600626 # Convert user-friendly command line option into a gerrit parameter.
627 def task(arg):
628 helper, cl = GetGerrit(opts, arg)
629 helper.SetReview(
630 cl,
631 labels={cls.LABEL: opts.value[0]},
632 msg=opts.msg,
633 dryrun=opts.dryrun,
634 notify=opts.notify,
635 )
636
637 _run_parallel_tasks(task, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500638
639
640class ActionLabelAutoSubmit(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600641 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500642
Alex Klein1699fab2022-09-08 08:46:06 -0600643 COMMAND = "label-as"
644 LABEL = "Auto-Submit"
645 VALUES = ("0", "1")
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600646
647
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500648class ActionLabelCodeReview(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600649 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500650
Alex Klein1699fab2022-09-08 08:46:06 -0600651 COMMAND = "label-cr"
652 LABEL = "Code-Review"
653 VALUES = ("-2", "-1", "0", "1", "2")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400654
655
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500656class ActionLabelVerified(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600657 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500658
Alex Klein1699fab2022-09-08 08:46:06 -0600659 COMMAND = "label-v"
660 LABEL = "Verified"
661 VALUES = ("-1", "0", "1")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400662
663
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500664class ActionLabelCommitQueue(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600665 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500666
Alex Klein1699fab2022-09-08 08:46:06 -0600667 COMMAND = "label-cq"
668 LABEL = "Commit-Queue"
669 VALUES = ("0", "1", "2")
670
Mike Frysinger15b23e42014-12-05 17:00:05 -0500671
C Shapiro3f1f8242021-08-02 15:28:29 -0500672class ActionLabelOwnersOverride(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600673 """Change the Owners-Override label (1=Override)"""
C Shapiro3f1f8242021-08-02 15:28:29 -0500674
Alex Klein1699fab2022-09-08 08:46:06 -0600675 COMMAND = "label-oo"
676 LABEL = "Owners-Override"
677 VALUES = ("0", "1")
C Shapiro3f1f8242021-08-02 15:28:29 -0500678
Mike Frysinger15b23e42014-12-05 17:00:05 -0500679
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500680class _ActionSimpleParallelCLs(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600681 """Base helper for actions that only accept CLs."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500682
Alex Klein1699fab2022-09-08 08:46:06 -0600683 @staticmethod
684 def init_subparser(parser):
685 """Add arguments to this action's subparser."""
686 parser.add_argument(
687 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
688 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500689
Alex Klein1699fab2022-09-08 08:46:06 -0600690 def __call__(self, opts):
691 """Implement the action."""
692
693 def task(arg):
694 helper, cl = GetGerrit(opts, arg)
695 self._process_one(helper, cl, opts)
696
697 _run_parallel_tasks(task, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500698
699
700class ActionSubmit(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600701 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500702
Alex Klein1699fab2022-09-08 08:46:06 -0600703 COMMAND = "submit"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500704
Alex Klein1699fab2022-09-08 08:46:06 -0600705 @staticmethod
706 def _process_one(helper, cl, opts):
707 """Use |helper| to process the single |cl|."""
708 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400709
710
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500711class ActionAbandon(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600712 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500713
Alex Klein1699fab2022-09-08 08:46:06 -0600714 COMMAND = "abandon"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500715
Alex Klein1699fab2022-09-08 08:46:06 -0600716 @staticmethod
717 def init_subparser(parser):
718 """Add arguments to this action's subparser."""
719 parser.add_argument(
720 "-m",
721 "--msg",
722 "--message",
723 metavar="MESSAGE",
724 help="Include a message",
725 )
726 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger3af378b2021-03-12 01:34:04 -0500727
Alex Klein1699fab2022-09-08 08:46:06 -0600728 @staticmethod
729 def _process_one(helper, cl, opts):
730 """Use |helper| to process the single |cl|."""
731 helper.AbandonChange(
732 cl, msg=opts.msg, dryrun=opts.dryrun, notify=opts.notify
733 )
Mike Frysinger13f23a42013-05-13 17:32:01 -0400734
735
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500736class ActionRestore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600737 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500738
Alex Klein1699fab2022-09-08 08:46:06 -0600739 COMMAND = "restore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500740
Alex Klein1699fab2022-09-08 08:46:06 -0600741 @staticmethod
742 def _process_one(helper, cl, opts):
743 """Use |helper| to process the single |cl|."""
744 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400745
746
Tomasz Figa54d70992021-01-20 13:48:59 +0900747class ActionWorkInProgress(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600748 """Mark CLs as work in progress"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900749
Alex Klein1699fab2022-09-08 08:46:06 -0600750 COMMAND = "wip"
Tomasz Figa54d70992021-01-20 13:48:59 +0900751
Alex Klein1699fab2022-09-08 08:46:06 -0600752 @staticmethod
753 def _process_one(helper, cl, opts):
754 """Use |helper| to process the single |cl|."""
755 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900756
757
758class ActionReadyForReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600759 """Mark CLs as ready for review"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900760
Alex Klein1699fab2022-09-08 08:46:06 -0600761 COMMAND = "ready"
Tomasz Figa54d70992021-01-20 13:48:59 +0900762
Alex Klein1699fab2022-09-08 08:46:06 -0600763 @staticmethod
764 def _process_one(helper, cl, opts):
765 """Use |helper| to process the single |cl|."""
766 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900767
768
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500769class ActionReviewers(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600770 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700771
Alex Klein1699fab2022-09-08 08:46:06 -0600772 COMMAND = "reviewers"
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700773
Alex Klein1699fab2022-09-08 08:46:06 -0600774 @staticmethod
775 def init_subparser(parser):
776 """Add arguments to this action's subparser."""
777 parser.add_argument("cl", metavar="CL", help="The CL to update")
778 parser.add_argument(
779 "reviewers", nargs="+", help="The reviewers to add/remove"
780 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700781
Alex Klein1699fab2022-09-08 08:46:06 -0600782 @staticmethod
783 def __call__(opts):
784 """Implement the action."""
785 # Allow for optional leading '~'.
786 email_validator = re.compile(r"^[~]?%s$" % constants.EMAIL_REGEX)
787 add_list, remove_list, invalid_list = [], [], []
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500788
Alex Klein1699fab2022-09-08 08:46:06 -0600789 for email in opts.reviewers:
790 if not email_validator.match(email):
791 invalid_list.append(email)
792 elif email[0] == "~":
793 remove_list.append(email[1:])
794 else:
795 add_list.append(email)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500796
Alex Klein1699fab2022-09-08 08:46:06 -0600797 if invalid_list:
798 cros_build_lib.Die(
799 "Invalid email address(es): %s" % ", ".join(invalid_list)
800 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500801
Alex Klein1699fab2022-09-08 08:46:06 -0600802 if add_list or remove_list:
803 helper, cl = GetGerrit(opts, opts.cl)
804 helper.SetReviewers(
805 cl,
806 add=add_list,
807 remove=remove_list,
808 dryrun=opts.dryrun,
809 notify=opts.notify,
810 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700811
812
Brian Norrisd25af082021-10-29 11:25:31 -0700813class ActionAttentionSet(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600814 """Add/remove emails from the attention set (prepend with '~' to remove)"""
Brian Norrisd25af082021-10-29 11:25:31 -0700815
Alex Klein1699fab2022-09-08 08:46:06 -0600816 COMMAND = "attention"
Brian Norrisd25af082021-10-29 11:25:31 -0700817
Alex Klein1699fab2022-09-08 08:46:06 -0600818 @staticmethod
819 def init_subparser(parser):
820 """Add arguments to this action's subparser."""
821 parser.add_argument(
822 "-m",
823 "--msg",
824 "--message",
825 metavar="MESSAGE",
826 help="Optional message to include",
827 default="gerrit CLI",
828 )
829 parser.add_argument("cl", metavar="CL", help="The CL to update")
830 parser.add_argument(
831 "users",
832 nargs="+",
833 help="The users to add/remove from attention set",
834 )
Brian Norrisd25af082021-10-29 11:25:31 -0700835
Alex Klein1699fab2022-09-08 08:46:06 -0600836 @staticmethod
837 def __call__(opts):
838 """Implement the action."""
839 # Allow for optional leading '~'.
840 email_validator = re.compile(r"^[~]?%s$" % constants.EMAIL_REGEX)
841 add_list, remove_list, invalid_list = [], [], []
Brian Norrisd25af082021-10-29 11:25:31 -0700842
Alex Klein1699fab2022-09-08 08:46:06 -0600843 for email in opts.users:
844 if not email_validator.match(email):
845 invalid_list.append(email)
846 elif email[0] == "~":
847 remove_list.append(email[1:])
848 else:
849 add_list.append(email)
Brian Norrisd25af082021-10-29 11:25:31 -0700850
Alex Klein1699fab2022-09-08 08:46:06 -0600851 if invalid_list:
852 cros_build_lib.Die(
853 "Invalid email address(es): %s" % ", ".join(invalid_list)
854 )
Brian Norrisd25af082021-10-29 11:25:31 -0700855
Alex Klein1699fab2022-09-08 08:46:06 -0600856 if add_list or remove_list:
857 helper, cl = GetGerrit(opts, opts.cl)
858 helper.SetAttentionSet(
859 cl,
860 add=add_list,
861 remove=remove_list,
862 dryrun=opts.dryrun,
863 notify=opts.notify,
864 message=opts.msg,
865 )
Brian Norrisd25af082021-10-29 11:25:31 -0700866
867
Mike Frysinger62178ae2020-03-20 01:37:43 -0400868class ActionMessage(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600869 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500870
Alex Klein1699fab2022-09-08 08:46:06 -0600871 COMMAND = "message"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500872
Alex Klein1699fab2022-09-08 08:46:06 -0600873 @staticmethod
874 def init_subparser(parser):
875 """Add arguments to this action's subparser."""
876 _ActionSimpleParallelCLs.init_subparser(parser)
877 parser.add_argument("message", help="The message to post")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500878
Alex Klein1699fab2022-09-08 08:46:06 -0600879 @staticmethod
880 def _process_one(helper, cl, opts):
881 """Use |helper| to process the single |cl|."""
882 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530883
884
Mike Frysinger62178ae2020-03-20 01:37:43 -0400885class ActionTopic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600886 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500887
Alex Klein1699fab2022-09-08 08:46:06 -0600888 COMMAND = "topic"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500889
Alex Klein1699fab2022-09-08 08:46:06 -0600890 @staticmethod
891 def init_subparser(parser):
892 """Add arguments to this action's subparser."""
893 _ActionSimpleParallelCLs.init_subparser(parser)
894 parser.add_argument("topic", help="The topic to set")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500895
Alex Klein1699fab2022-09-08 08:46:06 -0600896 @staticmethod
897 def _process_one(helper, cl, opts):
898 """Use |helper| to process the single |cl|."""
899 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800900
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800901
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500902class ActionPrivate(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600903 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700904
Alex Klein1699fab2022-09-08 08:46:06 -0600905 COMMAND = "private"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500906
Alex Klein1699fab2022-09-08 08:46:06 -0600907 @staticmethod
908 def _process_one(helper, cl, opts):
909 """Use |helper| to process the single |cl|."""
910 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700911
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800912
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500913class ActionPublic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600914 """Mark CLs public"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500915
Alex Klein1699fab2022-09-08 08:46:06 -0600916 COMMAND = "public"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500917
Alex Klein1699fab2022-09-08 08:46:06 -0600918 @staticmethod
919 def _process_one(helper, cl, opts):
920 """Use |helper| to process the single |cl|."""
921 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500922
923
924class ActionSethashtags(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600925 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500926
Alex Klein1699fab2022-09-08 08:46:06 -0600927 COMMAND = "hashtags"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500928
Alex Klein1699fab2022-09-08 08:46:06 -0600929 @staticmethod
930 def init_subparser(parser):
931 """Add arguments to this action's subparser."""
932 parser.add_argument("cl", metavar="CL", help="The CL to update")
933 parser.add_argument(
934 "hashtags", nargs="+", help="The hashtags to add/remove"
935 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500936
Alex Klein1699fab2022-09-08 08:46:06 -0600937 @staticmethod
938 def __call__(opts):
939 """Implement the action."""
940 add = []
941 remove = []
942 for hashtag in opts.hashtags:
943 if hashtag.startswith("~"):
944 remove.append(hashtag[1:])
945 else:
946 add.append(hashtag)
947 helper, cl = GetGerrit(opts, opts.cl)
948 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800949
950
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400951class ActionDelete(_ActionSimpleParallelCLs):
952 """Delete CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500953
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400954 COMMAND = "delete"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500955
Alex Klein1699fab2022-09-08 08:46:06 -0600956 @staticmethod
957 def _process_one(helper, cl, opts):
958 """Use |helper| to process the single |cl|."""
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400959 helper.Delete(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800960
961
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500962class ActionReviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600963 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500964
Alex Klein1699fab2022-09-08 08:46:06 -0600965 COMMAND = "reviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500966
Alex Klein1699fab2022-09-08 08:46:06 -0600967 @staticmethod
968 def _process_one(helper, cl, opts):
969 """Use |helper| to process the single |cl|."""
970 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500971
972
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500973class ActionUnreviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600974 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500975
Alex Klein1699fab2022-09-08 08:46:06 -0600976 COMMAND = "unreviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500977
Alex Klein1699fab2022-09-08 08:46:06 -0600978 @staticmethod
979 def _process_one(helper, cl, opts):
980 """Use |helper| to process the single |cl|."""
981 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500982
983
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500984class ActionIgnore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600985 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500986
Alex Klein1699fab2022-09-08 08:46:06 -0600987 COMMAND = "ignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500988
Alex Klein1699fab2022-09-08 08:46:06 -0600989 @staticmethod
990 def _process_one(helper, cl, opts):
991 """Use |helper| to process the single |cl|."""
992 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500993
994
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500995class ActionUnignore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600996 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500997
Alex Klein1699fab2022-09-08 08:46:06 -0600998 COMMAND = "unignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500999
Alex Klein1699fab2022-09-08 08:46:06 -06001000 @staticmethod
1001 def _process_one(helper, cl, opts):
1002 """Use |helper| to process the single |cl|."""
1003 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -05001004
1005
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001006class ActionCherryPick(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001007 """Cherry-pick CLs to branches."""
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001008
Alex Klein1699fab2022-09-08 08:46:06 -06001009 COMMAND = "cherry-pick"
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001010
Alex Klein1699fab2022-09-08 08:46:06 -06001011 @staticmethod
1012 def init_subparser(parser):
1013 """Add arguments to this action's subparser."""
1014 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
1015 parser.add_argument(
1016 "--rev",
1017 "--revision",
1018 default="current",
1019 help="A specific revision or patchset",
1020 )
1021 parser.add_argument(
1022 "-m",
1023 "--msg",
1024 "--message",
1025 metavar="MESSAGE",
1026 help="Include a message",
1027 )
1028 parser.add_argument(
1029 "--branches",
1030 "--branch",
1031 "--br",
1032 action="split_extend",
1033 default=[],
1034 required=True,
1035 help="The destination branches",
1036 )
1037 parser.add_argument(
1038 "cls", nargs="+", metavar="CL", help="The CLs to cherry-pick"
1039 )
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001040
Alex Klein1699fab2022-09-08 08:46:06 -06001041 @staticmethod
1042 def __call__(opts):
1043 """Implement the action."""
Mike Frysinger16474792023-03-01 01:18:00 -05001044
Alex Klein1699fab2022-09-08 08:46:06 -06001045 # Process branches in parallel, but CLs in serial in case of CL stacks.
1046 def task(branch):
1047 for arg in opts.cls:
1048 helper, cl = GetGerrit(opts, arg)
1049 ret = helper.CherryPick(
1050 cl,
1051 branch,
1052 rev=opts.rev,
1053 msg=opts.msg,
1054 dryrun=opts.dryrun,
1055 notify=opts.notify,
1056 )
1057 logging.debug("Response: %s", ret)
1058 if opts.format is OutputFormat.RAW:
1059 print(ret["_number"])
1060 else:
1061 uri = f'https://{helper.host}/c/{ret["_number"]}'
1062 print(uri_lib.ShortenUri(uri))
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001063
Alex Klein1699fab2022-09-08 08:46:06 -06001064 _run_parallel_tasks(task, *opts.branches)
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001065
1066
Mike Frysinger8037f752020-02-29 20:47:09 -05001067class ActionReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001068 """Review CLs with multiple settings
Mike Frysinger8037f752020-02-29 20:47:09 -05001069
Alex Klein4507b172023-01-13 11:39:51 -07001070 The label option supports extended/multiple syntax for easy use. The --label
1071 option may be specified multiple times (as settings are merges), and
1072 multiple labels are allowed in a single argument. Each label has the form:
Alex Klein1699fab2022-09-08 08:46:06 -06001073 <long or short name><=+-><value>
Mike Frysinger8037f752020-02-29 20:47:09 -05001074
Alex Klein1699fab2022-09-08 08:46:06 -06001075 Common arguments:
1076 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
1077 'V+1 CQ+2'
1078 'AS=1 V=1'
1079 """
Mike Frysinger8037f752020-02-29 20:47:09 -05001080
Alex Klein1699fab2022-09-08 08:46:06 -06001081 COMMAND = "review"
Mike Frysinger8037f752020-02-29 20:47:09 -05001082
Alex Klein1699fab2022-09-08 08:46:06 -06001083 class _SetLabel(argparse.Action):
1084 """Argparse action for setting labels."""
Mike Frysinger8037f752020-02-29 20:47:09 -05001085
Alex Klein1699fab2022-09-08 08:46:06 -06001086 LABEL_MAP = {
1087 "AS": "Auto-Submit",
1088 "CQ": "Commit-Queue",
1089 "CR": "Code-Review",
1090 "V": "Verified",
1091 }
Mike Frysinger8037f752020-02-29 20:47:09 -05001092
Alex Klein1699fab2022-09-08 08:46:06 -06001093 def __call__(self, parser, namespace, values, option_string=None):
1094 labels = getattr(namespace, self.dest)
1095 for request in values.split():
1096 if "=" 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 elif "-" in request:
1103 # Handle Verified-1 form.
1104 short, value = request.split("-", 1)
1105 value = "-%s" % (value,)
1106 else:
1107 parser.error(
1108 'Invalid label setting "%s". Must be Commit-Queue=1 or '
1109 "CQ+1 or CR-1." % (request,)
1110 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001111
Alex Klein1699fab2022-09-08 08:46:06 -06001112 # Convert possible short label names like "V" to "Verified".
1113 label = self.LABEL_MAP.get(short)
1114 if not label:
1115 label = short
Mike Frysinger8037f752020-02-29 20:47:09 -05001116
Alex Klein1699fab2022-09-08 08:46:06 -06001117 # We allow existing label requests to be overridden.
1118 labels[label] = value
Mike Frysinger8037f752020-02-29 20:47:09 -05001119
Alex Klein1699fab2022-09-08 08:46:06 -06001120 @classmethod
1121 def init_subparser(cls, parser):
1122 """Add arguments to this action's subparser."""
1123 parser.add_argument(
1124 "-m",
1125 "--msg",
1126 "--message",
1127 metavar="MESSAGE",
1128 help="Include a message",
1129 )
1130 parser.add_argument(
1131 "-l",
1132 "--label",
1133 dest="labels",
1134 action=cls._SetLabel,
1135 default={},
1136 help="Set a label with a value",
1137 )
1138 parser.add_argument(
1139 "--ready",
1140 default=None,
1141 action="store_true",
1142 help="Set CL status to ready-for-review",
1143 )
1144 parser.add_argument(
1145 "--wip",
1146 default=None,
1147 action="store_true",
1148 help="Set CL status to WIP",
1149 )
1150 parser.add_argument(
1151 "--reviewers",
1152 "--re",
1153 action="append",
1154 default=[],
1155 help="Add reviewers",
1156 )
1157 parser.add_argument(
1158 "--cc", action="append", default=[], help="Add people to CC"
1159 )
1160 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger8037f752020-02-29 20:47:09 -05001161
Alex Klein1699fab2022-09-08 08:46:06 -06001162 @staticmethod
1163 def _process_one(helper, cl, opts):
1164 """Use |helper| to process the single |cl|."""
1165 helper.SetReview(
1166 cl,
1167 msg=opts.msg,
1168 labels=opts.labels,
1169 dryrun=opts.dryrun,
1170 notify=opts.notify,
1171 reviewers=opts.reviewers,
1172 cc=opts.cc,
1173 ready=opts.ready,
1174 wip=opts.wip,
1175 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001176
1177
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001178class ActionAccount(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001179 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001180
Alex Klein1699fab2022-09-08 08:46:06 -06001181 COMMAND = "account"
1182 USE_PAGER = True
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001183
Alex Klein1699fab2022-09-08 08:46:06 -06001184 @staticmethod
1185 def init_subparser(parser):
1186 """Add arguments to this action's subparser."""
1187 parser.add_argument(
1188 "accounts",
1189 nargs="*",
1190 default=["self"],
1191 help="The accounts to query",
1192 )
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001193
Alex Klein1699fab2022-09-08 08:46:06 -06001194 @classmethod
1195 def __call__(cls, opts):
1196 """Implement the action."""
1197 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001198
Alex Klein1699fab2022-09-08 08:46:06 -06001199 def print_one(header, data):
1200 print(f"### {header}")
1201 compact = opts.format is OutputFormat.JSON
1202 print(pformat.json(data, compact=compact).rstrip())
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001203
Alex Klein1699fab2022-09-08 08:46:06 -06001204 def task(arg):
1205 detail = gob_util.FetchUrlJson(
1206 helper.host, f"accounts/{arg}/detail"
1207 )
1208 if not detail:
1209 print(f"{arg}: account not found")
1210 else:
1211 print_one("detail", detail)
1212 for field in (
Mike Frysinger03c75072023-03-10 17:25:21 -05001213 "external.ids",
Alex Klein1699fab2022-09-08 08:46:06 -06001214 "groups",
1215 "capabilities",
1216 "preferences",
1217 "sshkeys",
1218 "gpgkeys",
1219 ):
1220 data = gob_util.FetchUrlJson(
1221 helper.host, f"accounts/{arg}/{field}"
1222 )
1223 print_one(field, data)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001224
Alex Klein1699fab2022-09-08 08:46:06 -06001225 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -08001226
1227
Mike Frysinger2295d792021-03-08 15:55:23 -05001228class ActionConfig(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001229 """Manage the gerrit tool's own config file
Mike Frysinger2295d792021-03-08 15:55:23 -05001230
Alex Klein1699fab2022-09-08 08:46:06 -06001231 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
1232 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
Mike Frysinger2295d792021-03-08 15:55:23 -05001233
Alex Klein1699fab2022-09-08 08:46:06 -06001234 # Set up subcommand aliases.
1235 [alias]
1236 common-search = search 'is:open project:something/i/care/about'
1237 """
Mike Frysinger2295d792021-03-08 15:55:23 -05001238
Alex Klein1699fab2022-09-08 08:46:06 -06001239 COMMAND = "config"
Mike Frysinger2295d792021-03-08 15:55:23 -05001240
Alex Klein1699fab2022-09-08 08:46:06 -06001241 @staticmethod
1242 def __call__(opts):
1243 """Implement the action."""
Alex Klein4507b172023-01-13 11:39:51 -07001244 # For now, this is a place holder for raising visibility for the config
1245 # file and its associated help text documentation.
Alex Klein1699fab2022-09-08 08:46:06 -06001246 opts.parser.parse_args(["config", "--help"])
Mike Frysinger2295d792021-03-08 15:55:23 -05001247
1248
Mike Frysingere5450602021-03-08 15:34:17 -05001249class ActionHelp(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001250 """An alias to --help for CLI symmetry"""
Mike Frysingere5450602021-03-08 15:34:17 -05001251
Alex Klein1699fab2022-09-08 08:46:06 -06001252 COMMAND = "help"
1253 USE_PAGER = True
Mike Frysingere5450602021-03-08 15:34:17 -05001254
Alex Klein1699fab2022-09-08 08:46:06 -06001255 @staticmethod
1256 def init_subparser(parser):
1257 """Add arguments to this action's subparser."""
1258 parser.add_argument(
1259 "command", nargs="?", help="The command to display."
1260 )
Mike Frysingere5450602021-03-08 15:34:17 -05001261
Alex Klein1699fab2022-09-08 08:46:06 -06001262 @staticmethod
1263 def __call__(opts):
1264 """Implement the action."""
1265 # Show global help.
1266 if not opts.command:
1267 opts.parser.print_help()
1268 return
Mike Frysingere5450602021-03-08 15:34:17 -05001269
Alex Klein1699fab2022-09-08 08:46:06 -06001270 opts.parser.parse_args([opts.command, "--help"])
Mike Frysingere5450602021-03-08 15:34:17 -05001271
1272
Mike Frysinger484e2f82020-03-20 01:41:10 -04001273class ActionHelpAll(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001274 """Show all actions help output at once."""
Mike Frysinger484e2f82020-03-20 01:41:10 -04001275
Alex Klein1699fab2022-09-08 08:46:06 -06001276 COMMAND = "help-all"
1277 USE_PAGER = True
Mike Frysinger484e2f82020-03-20 01:41:10 -04001278
Alex Klein1699fab2022-09-08 08:46:06 -06001279 @staticmethod
1280 def __call__(opts):
1281 """Implement the action."""
1282 first = True
1283 for action in _GetActions():
1284 if first:
1285 first = False
1286 else:
1287 print("\n\n")
Mike Frysinger484e2f82020-03-20 01:41:10 -04001288
Alex Klein1699fab2022-09-08 08:46:06 -06001289 try:
1290 opts.parser.parse_args([action, "--help"])
1291 except SystemExit:
1292 pass
Mike Frysinger484e2f82020-03-20 01:41:10 -04001293
1294
Mike Frysinger65fc8632020-02-06 18:11:12 -05001295@memoize.Memoize
1296def _GetActions():
Alex Klein1699fab2022-09-08 08:46:06 -06001297 """Get all the possible actions we support.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001298
Alex Klein1699fab2022-09-08 08:46:06 -06001299 Returns:
1300 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1301 function that implements that command (e.g. UserActFoo).
1302 """
1303 VALID_NAME = re.compile(r"^[a-z][a-z-]*[a-z]$")
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001304
Alex Klein1699fab2022-09-08 08:46:06 -06001305 actions = {}
1306 for cls in globals().values():
1307 if (
1308 not inspect.isclass(cls)
1309 or not issubclass(cls, UserAction)
1310 or not getattr(cls, "COMMAND", None)
1311 ):
1312 continue
Mike Frysinger65fc8632020-02-06 18:11:12 -05001313
Alex Klein1699fab2022-09-08 08:46:06 -06001314 # Sanity check names for devs adding new commands. Should be quick.
1315 cmd = cls.COMMAND
1316 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1317 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001318
Alex Klein1699fab2022-09-08 08:46:06 -06001319 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001320
Alex Klein1699fab2022-09-08 08:46:06 -06001321 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001322
1323
Harry Cutts26076b32019-02-26 15:01:29 -08001324def _GetActionUsages():
Alex Klein1699fab2022-09-08 08:46:06 -06001325 """Formats a one-line usage and doc message for each action."""
1326 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001327
Alex Klein1699fab2022-09-08 08:46:06 -06001328 cmds = list(actions.keys())
1329 functions = list(actions.values())
1330 usages = [getattr(x, "usage", "") for x in functions]
1331 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001332
Alex Klein1699fab2022-09-08 08:46:06 -06001333 cmd_indent = len(max(cmds, key=len))
1334 usage_indent = len(max(usages, key=len))
1335 return "\n".join(
1336 " %-*s %-*s : %s" % (cmd_indent, cmd, usage_indent, usage, doc)
1337 for cmd, usage, doc in zip(cmds, usages, docs)
1338 )
Harry Cutts26076b32019-02-26 15:01:29 -08001339
1340
Mike Frysinger2295d792021-03-08 15:55:23 -05001341def _AddCommonOptions(parser, subparser):
Alex Klein1699fab2022-09-08 08:46:06 -06001342 """Add options that should work before & after the subcommand.
Mike Frysinger2295d792021-03-08 15:55:23 -05001343
Alex Klein1699fab2022-09-08 08:46:06 -06001344 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1345 """
1346 parser.add_common_argument_to_group(
1347 subparser,
1348 "--ne",
1349 "--no-emails",
1350 dest="notify",
1351 default="ALL",
1352 action="store_const",
1353 const="NONE",
1354 help="Do not send e-mail notifications",
1355 )
1356 parser.add_common_argument_to_group(
1357 subparser,
1358 "-n",
1359 "--dry-run",
1360 dest="dryrun",
1361 default=False,
1362 action="store_true",
1363 help="Show what would be done, but do not make changes",
1364 )
Mike Frysinger2295d792021-03-08 15:55:23 -05001365
1366
1367def GetBaseParser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001368 """Returns the common parser (i.e. no subparsers added)."""
1369 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001370There is no support for doing line-by-line code review via the command line.
1371This helps you manage various bits and CL status.
1372
Mike Frysingera1db2c42014-06-15 00:42:48 -07001373For general Gerrit documentation, see:
1374 https://gerrit-review.googlesource.com/Documentation/
1375The Searching Changes page covers the search query syntax:
1376 https://gerrit-review.googlesource.com/Documentation/user-search.html
1377
Mike Frysinger13f23a42013-05-13 17:32:01 -04001378Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001379 $ gerrit todo # List all the CLs that await your review.
1380 $ gerrit mine # List all of your open CLs.
1381 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1382 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1383 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001384 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
138528123.
1386 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1387CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001388Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001389 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1390with Commit-Queue=1.
1391 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1392CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001393 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001394
Harry Cutts26076b32019-02-26 15:01:29 -08001395Actions:
1396"""
Alex Klein1699fab2022-09-08 08:46:06 -06001397 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001398
Alex Klein1699fab2022-09-08 08:46:06 -06001399 site_params = config_lib.GetSiteParams()
1400 parser = commandline.ArgumentParser(
1401 description=description,
1402 default_log_level="notice",
1403 epilog="For subcommand help, use `gerrit help <command>`.",
1404 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001405
Alex Klein1699fab2022-09-08 08:46:06 -06001406 group = parser.add_argument_group("Server options")
1407 group.add_argument(
1408 "-i",
1409 "--internal",
1410 dest="gob",
1411 action="store_const",
1412 default=site_params.EXTERNAL_GOB_INSTANCE,
1413 const=site_params.INTERNAL_GOB_INSTANCE,
1414 help="Query internal Chrome Gerrit instance",
1415 )
1416 group.add_argument(
1417 "-g",
1418 "--gob",
1419 default=site_params.EXTERNAL_GOB_INSTANCE,
1420 help=("Gerrit (on borg) instance to query " "(default: %(default)s)"),
1421 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001422
Alex Klein1699fab2022-09-08 08:46:06 -06001423 group = parser.add_argument_group("CL options")
1424 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001425
Alex Klein1699fab2022-09-08 08:46:06 -06001426 group = parser.add_mutually_exclusive_group()
1427 parser.set_defaults(format=OutputFormat.AUTO)
1428 group.add_argument(
1429 "--format",
1430 action="enum",
1431 enum=OutputFormat,
1432 help="Output format to use.",
1433 )
1434 group.add_argument(
1435 "--raw",
1436 action="store_const",
1437 dest="format",
1438 const=OutputFormat.RAW,
1439 help="Alias for --format=raw.",
1440 )
1441 group.add_argument(
1442 "--json",
1443 action="store_const",
1444 dest="format",
1445 const=OutputFormat.JSON,
1446 help="Alias for --format=json.",
1447 )
Jack Rosenthal95aac172022-06-30 15:35:07 -06001448
Alex Klein1699fab2022-09-08 08:46:06 -06001449 group = parser.add_mutually_exclusive_group()
1450 group.add_argument(
1451 "--pager",
1452 action="store_true",
1453 default=sys.stdout.isatty(),
1454 help="Enable pager.",
1455 )
1456 group.add_argument(
1457 "--no-pager", action="store_false", dest="pager", help="Disable pager."
1458 )
1459 return parser
Mike Frysinger2295d792021-03-08 15:55:23 -05001460
1461
Alex Klein1699fab2022-09-08 08:46:06 -06001462def GetParser(
1463 parser: commandline.ArgumentParser = None,
Mike Frysinger16474792023-03-01 01:18:00 -05001464) -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001465 """Returns the full parser to use for this module."""
1466 if parser is None:
1467 parser = GetBaseParser()
Mike Frysinger2295d792021-03-08 15:55:23 -05001468
Alex Klein1699fab2022-09-08 08:46:06 -06001469 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001470
Alex Klein1699fab2022-09-08 08:46:06 -06001471 # Subparsers are required by default under Python 2. Python 3 changed to
1472 # not required, but didn't include a required option until 3.7. Setting
1473 # the required member works in all versions (and setting dest name).
1474 subparsers = parser.add_subparsers(dest="action")
1475 subparsers.required = True
1476 for cmd, cls in actions.items():
1477 # Format the full docstring by removing the file level indentation.
1478 description = re.sub(r"^ ", "", cls.__doc__, flags=re.M)
1479 subparser = subparsers.add_parser(cmd, description=description)
1480 _AddCommonOptions(parser, subparser)
1481 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001482
Alex Klein1699fab2022-09-08 08:46:06 -06001483 return parser
Mike Frysinger108eda22018-06-06 18:45:12 -04001484
1485
Jack Rosenthal95aac172022-06-30 15:35:07 -06001486def start_pager():
Alex Klein1699fab2022-09-08 08:46:06 -06001487 """Re-spawn ourselves attached to a pager."""
1488 pager = os.environ.get("PAGER", "less")
1489 os.environ.setdefault("LESS", "FRX")
Jack Rosenthal95aac172022-06-30 15:35:07 -06001490 with subprocess.Popen(
Alex Klein1699fab2022-09-08 08:46:06 -06001491 # sys.argv can have some edge cases: we may not necessarily use
1492 # sys.executable if the script is executed as "python path/to/script".
1493 # If we upgrade to Python 3.10+, this should be changed to sys.orig_argv
1494 # for full accuracy.
1495 sys.argv,
1496 stdout=subprocess.PIPE,
1497 stderr=subprocess.STDOUT,
1498 env={"GERRIT_RESPAWN_FOR_PAGER": "1", **os.environ},
1499 ) as gerrit_proc:
1500 with subprocess.Popen(
1501 pager,
1502 shell=True,
1503 stdin=gerrit_proc.stdout,
1504 ) as pager_proc:
1505 # Send SIGINT to just the gerrit process, not the pager too.
1506 def _sighandler(signum, _frame):
1507 gerrit_proc.send_signal(signum)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001508
Alex Klein1699fab2022-09-08 08:46:06 -06001509 signal.signal(signal.SIGINT, _sighandler)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001510
Alex Klein1699fab2022-09-08 08:46:06 -06001511 pager_proc.communicate()
1512 # If the pager exits, and the gerrit process is still running, we
1513 # must terminate it.
1514 if gerrit_proc.poll() is None:
1515 gerrit_proc.terminate()
1516 sys.exit(gerrit_proc.wait())
Jack Rosenthal95aac172022-06-30 15:35:07 -06001517
1518
Mike Frysinger108eda22018-06-06 18:45:12 -04001519def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -06001520 base_parser = GetBaseParser()
1521 opts, subargs = base_parser.parse_known_args(argv)
Mike Frysinger2295d792021-03-08 15:55:23 -05001522
Alex Klein1699fab2022-09-08 08:46:06 -06001523 config = Config()
1524 if subargs:
Alex Klein4507b172023-01-13 11:39:51 -07001525 # If the action is an alias to an expanded value, we need to mutate the
1526 # argv and reparse things.
Alex Klein1699fab2022-09-08 08:46:06 -06001527 action = config.expand_alias(subargs[0])
1528 if action != subargs[0]:
1529 pos = argv.index(subargs[0])
1530 argv = argv[:pos] + action + argv[pos + 1 :]
Mike Frysinger2295d792021-03-08 15:55:23 -05001531
Alex Klein1699fab2022-09-08 08:46:06 -06001532 parser = GetParser(parser=base_parser)
1533 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001534
Alex Klein1699fab2022-09-08 08:46:06 -06001535 # If we're running as a re-spawn for the pager, from this point on
1536 # we'll pretend we're attached to a TTY. This will give us colored
1537 # output when requested.
1538 if os.environ.pop("GERRIT_RESPAWN_FOR_PAGER", None) is not None:
1539 opts.pager = False
1540 sys.stdout.isatty = lambda: True
Jack Rosenthal95aac172022-06-30 15:35:07 -06001541
Alex Klein1699fab2022-09-08 08:46:06 -06001542 # In case the action wants to throw a parser error.
1543 opts.parser = parser
Mike Frysinger484e2f82020-03-20 01:41:10 -04001544
Alex Klein1699fab2022-09-08 08:46:06 -06001545 # A cache of gerrit helpers we'll load on demand.
1546 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001547
Alex Klein1699fab2022-09-08 08:46:06 -06001548 if opts.format is OutputFormat.AUTO:
1549 if sys.stdout.isatty():
1550 opts.format = OutputFormat.PRETTY
1551 else:
1552 opts.format = OutputFormat.RAW
Jack Rosenthale3a92672022-06-29 14:54:48 -06001553
Alex Klein1699fab2022-09-08 08:46:06 -06001554 opts.Freeze()
Mike Frysinger88f27292014-06-17 09:40:45 -07001555
Alex Klein1699fab2022-09-08 08:46:06 -06001556 # pylint: disable=global-statement
1557 global COLOR
1558 COLOR = terminal.Color(enabled=opts.color)
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001559
Alex Klein1699fab2022-09-08 08:46:06 -06001560 # Now look up the requested user action and run it.
1561 actions = _GetActions()
1562 action_class = actions[opts.action]
1563 if action_class.USE_PAGER and opts.pager:
1564 start_pager()
1565 obj = action_class()
1566 try:
1567 obj(opts)
1568 except (
1569 cros_build_lib.RunCommandError,
1570 gerrit.GerritException,
1571 gob_util.GOBError,
1572 ) as e:
1573 cros_build_lib.Die(e)