blob: cd4163c66fc60bb632ea35fe7bc0be0ea94f7cb7 [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 Frysinger1a66eda2023-08-21 11:18:47 -040027from typing import List, Set, Tuple
Mike Frysinger13f23a42013-05-13 17:32:01 -040028
Mike Frysinger2295d792021-03-08 15:55:23 -050029from chromite.lib import chromite_config
Chris McDonald59650c32021-07-20 15:29:28 -060030from chromite.lib import commandline
Aviv Keshetb7519e12016-10-04 00:50:00 -070031from chromite.lib import config_lib
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
Alex Klein074f94f2023-06-22 10:32:06 -060063class UserAction:
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 Frysinger5d615452023-08-21 10:51:32 -0400165def _run_parallel_tasks(task, jobs: int, *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
Mike Frysinger5d615452023-08-21 10:51:32 -0400184 with parallel.BackgroundTaskRunner(retry, processes=jobs) as q:
Alex Klein1699fab2022-09-08 08:46:06 -0600185 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 Frysinger1a66eda2023-08-21 11:18:47 -0400204def process_add_remove_lists(items: List[str]) -> Tuple[Set[str], Set[str]]:
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400205 """Split |items| into "add" and "remove" lists.
206
207 Invalid items will cause the program to exit with an error message.
208
209 Args:
Mike Frysingerd74a8bc2023-06-22 09:42:55 -0400210 items: Items that begin with "~" or "-" mean "remove" while others are
211 "add".
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400212
213 Returns:
214 A tuple of sets: all the items to add and all the items to remove.
Mike Frysingerd74a8bc2023-06-22 09:42:55 -0400215 NB: The leading "~" & "-" will automatically be stripped.
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400216 """
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400217 add_list, remove_list, invalid_list = set(), set(), set()
218 for item in items:
219 if not item:
220 invalid_list.add(item)
221 continue
222
223 remove = False
Mike Frysingerd74a8bc2023-06-22 09:42:55 -0400224 if item[0] in ("~", "-"):
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400225 remove = True
226 item = item[1:]
227
Mike Frysinger1a66eda2023-08-21 11:18:47 -0400228 if remove:
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400229 remove_list.add(item)
230 add_list.discard(item)
231 else:
232 add_list.add(item)
233 remove_list.discard(item)
234
235 if invalid_list:
236 cros_build_lib.Die("Invalid arguments: %s", ", ".join(invalid_list))
237
238 return (add_list, remove_list)
239
240
Mike Frysinger88f27292014-06-17 09:40:45 -0700241# TODO: This func really needs to be merged into the core gerrit logic.
242def GetGerrit(opts, cl=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600243 """Auto pick the right gerrit instance based on the |cl|
Mike Frysinger88f27292014-06-17 09:40:45 -0700244
Alex Klein1699fab2022-09-08 08:46:06 -0600245 Args:
Trent Apted66736d82023-05-25 10:38:28 +1000246 opts: The general options object.
247 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
Mike Frysinger88f27292014-06-17 09:40:45 -0700248
Alex Klein1699fab2022-09-08 08:46:06 -0600249 Returns:
Trent Apted66736d82023-05-25 10:38:28 +1000250 A tuple of a gerrit object and a sanitized CL #.
Alex Klein1699fab2022-09-08 08:46:06 -0600251 """
252 gob = opts.gob
253 if cl is not None:
254 if cl.startswith("*") or cl.startswith("chrome-internal:"):
255 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
256 if cl.startswith("*"):
257 cl = cl[1:]
258 else:
259 cl = cl[16:]
260 elif ":" in cl:
261 gob, cl = cl.split(":", 1)
Mike Frysinger88f27292014-06-17 09:40:45 -0700262
Alex Klein1699fab2022-09-08 08:46:06 -0600263 if not gob in opts.gerrit:
264 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
Mike Frysinger88f27292014-06-17 09:40:45 -0700265
Alex Klein1699fab2022-09-08 08:46:06 -0600266 return (opts.gerrit[gob], cl)
Mike Frysinger88f27292014-06-17 09:40:45 -0700267
268
Mike Frysinger13f23a42013-05-13 17:32:01 -0400269def GetApprovalSummary(_opts, cls):
Alex Klein1699fab2022-09-08 08:46:06 -0600270 """Return a dict of the most important approvals"""
Alex Kleine37b8762023-04-17 12:16:15 -0600271 approvs = {x: "" for x in GERRIT_SUMMARY_CATS}
Alex Klein1699fab2022-09-08 08:46:06 -0600272 for approver in cls.get("currentPatchSet", {}).get("approvals", []):
273 cats = GERRIT_APPROVAL_MAP.get(approver["type"])
274 if not cats:
275 logging.warning(
276 "unknown gerrit approval type: %s", approver["type"]
277 )
278 continue
279 cat = cats[0].strip()
280 val = int(approver["value"])
281 if not cat in approvs:
282 # Ignore the extended categories in the summary view.
283 continue
284 elif approvs[cat] == "":
285 approvs[cat] = val
286 elif val < 0:
287 approvs[cat] = min(approvs[cat], val)
288 else:
289 approvs[cat] = max(approvs[cat], val)
290 return approvs
Mike Frysinger13f23a42013-05-13 17:32:01 -0400291
292
Mike Frysingera1b4b272017-04-05 16:11:00 -0400293def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600294 """Pretty print a single result"""
295 if lims is None:
296 lims = {"url": 0, "project": 0}
Mike Frysinger13f23a42013-05-13 17:32:01 -0400297
Alex Klein1699fab2022-09-08 08:46:06 -0600298 status = ""
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400299
Alex Klein1699fab2022-09-08 08:46:06 -0600300 if opts.verbose:
301 status += "%s " % (cl["status"],)
302 else:
303 status += "%s " % (GERRIT_SUMMARY_MAP.get(cl["status"], cl["status"]),)
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400304
Alex Klein1699fab2022-09-08 08:46:06 -0600305 if show_approvals and not opts.verbose:
306 approvs = GetApprovalSummary(opts, cl)
307 for cat in GERRIT_SUMMARY_CATS:
308 if approvs[cat] in ("", 0):
309 functor = lambda x: x
310 elif approvs[cat] < 0:
311 functor = red
312 else:
313 functor = green
314 status += functor("%s:%2s " % (cat, approvs[cat]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400315
Alex Klein1699fab2022-09-08 08:46:06 -0600316 if opts.format is OutputFormat.MARKDOWN:
317 print("* %s - %s" % (uri_lib.ShortenUri(cl["url"]), cl["subject"]))
318 else:
319 print(
320 "%s %s%-*s %s"
321 % (
322 blue("%-*s" % (lims["url"], cl["url"])),
323 status,
324 lims["project"],
325 cl["project"],
326 cl["subject"],
327 )
328 )
Mike Frysinger13f23a42013-05-13 17:32:01 -0400329
Alex Klein1699fab2022-09-08 08:46:06 -0600330 if show_approvals and opts.verbose:
331 for approver in cl["currentPatchSet"].get("approvals", []):
332 functor = red if int(approver["value"]) < 0 else green
333 n = functor("%2s" % approver["value"])
334 t = GERRIT_APPROVAL_MAP.get(
335 approver["type"], [approver["type"], approver["type"]]
336 )[1]
337 print(" %s %s %s" % (n, t, approver["by"]["email"]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400338
339
Mike Frysingera1b4b272017-04-05 16:11:00 -0400340def PrintCls(opts, cls, lims=None, show_approvals=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600341 """Print all results based on the requested format."""
342 if opts.format is OutputFormat.RAW:
343 site_params = config_lib.GetSiteParams()
344 pfx = ""
345 # Special case internal Chrome GoB as that is what most devs use.
346 # They can always redirect the list elsewhere via the -g option.
347 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
348 pfx = site_params.INTERNAL_CHANGE_PREFIX
349 for cl in cls:
350 print("%s%s" % (pfx, cl["number"]))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400351
Alex Klein1699fab2022-09-08 08:46:06 -0600352 elif opts.format is OutputFormat.JSON:
353 json.dump(cls, sys.stdout)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400354
Alex Klein1699fab2022-09-08 08:46:06 -0600355 else:
356 if lims is None:
357 lims = limits(cls)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400358
Alex Klein1699fab2022-09-08 08:46:06 -0600359 for cl in cls:
360 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400361
362
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400363def _Query(opts, query, raw=True, helper=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600364 """Queries Gerrit with a query string built from the commandline options"""
365 if opts.branch is not None:
366 query += " branch:%s" % opts.branch
367 if opts.project is not None:
368 query += " project: %s" % opts.project
369 if opts.topic is not None:
370 query += " topic: %s" % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800371
Alex Klein1699fab2022-09-08 08:46:06 -0600372 if helper is None:
373 helper, _ = GetGerrit(opts)
374 return helper.Query(query, raw=raw, bypass_cache=False)
Paul Hobbs89765232015-06-24 14:07:49 -0700375
376
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400377def FilteredQuery(opts, query, helper=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600378 """Query gerrit and filter/clean up the results"""
379 ret = []
Paul Hobbs89765232015-06-24 14:07:49 -0700380
Alex Klein1699fab2022-09-08 08:46:06 -0600381 logging.debug("Running query: %s", query)
382 for cl in _Query(opts, query, raw=True, helper=helper):
383 # Gerrit likes to return a stats record too.
384 if not "project" in cl:
385 continue
Mike Frysinger13f23a42013-05-13 17:32:01 -0400386
Alex Klein1699fab2022-09-08 08:46:06 -0600387 # Strip off common leading names since the result is still
388 # unique over the whole tree.
389 if not opts.verbose:
390 for pfx in (
391 "aosp",
392 "chromeos",
393 "chromiumos",
394 "external",
395 "overlays",
396 "platform",
397 "third_party",
398 ):
399 if cl["project"].startswith("%s/" % pfx):
400 cl["project"] = cl["project"][len(pfx) + 1 :]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400401
Alex Klein1699fab2022-09-08 08:46:06 -0600402 cl["url"] = uri_lib.ShortenUri(cl["url"])
Mike Frysinger479f1192017-09-14 22:36:30 -0400403
Alex Klein1699fab2022-09-08 08:46:06 -0600404 ret.append(cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400405
Alex Klein1699fab2022-09-08 08:46:06 -0600406 if opts.sort == "unsorted":
407 return ret
408 if opts.sort == "number":
409 key = lambda x: int(x[opts.sort])
410 else:
411 key = lambda x: x[opts.sort]
412 return sorted(ret, key=key)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400413
414
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500415class _ActionSearchQuery(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600416 """Base class for actions that perform searches."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500417
Alex Klein1699fab2022-09-08 08:46:06 -0600418 USE_PAGER = True
Jack Rosenthal95aac172022-06-30 15:35:07 -0600419
Alex Klein1699fab2022-09-08 08:46:06 -0600420 @staticmethod
421 def init_subparser(parser):
422 """Add arguments to this action's subparser."""
423 parser.add_argument(
424 "--sort",
425 default="number",
Trent Apted66736d82023-05-25 10:38:28 +1000426 help='Key to sort on (number, project); use "unsorted" to disable',
Alex Klein1699fab2022-09-08 08:46:06 -0600427 )
428 parser.add_argument(
429 "-b", "--branch", help="Limit output to the specific branch"
430 )
431 parser.add_argument(
432 "-p", "--project", help="Limit output to the specific project"
433 )
434 parser.add_argument(
435 "-t", "--topic", help="Limit output to the specific topic"
436 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500437
438
439class ActionTodo(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600440 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500441
Alex Klein1699fab2022-09-08 08:46:06 -0600442 COMMAND = "todo"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500443
Alex Klein1699fab2022-09-08 08:46:06 -0600444 @staticmethod
445 def __call__(opts):
446 """Implement the action."""
447 cls = FilteredQuery(opts, "attention:self")
448 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400449
450
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500451class ActionSearch(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600452 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500453
Alex Klein1699fab2022-09-08 08:46:06 -0600454 COMMAND = "search"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500455
Alex Klein1699fab2022-09-08 08:46:06 -0600456 @staticmethod
457 def init_subparser(parser):
458 """Add arguments to this action's subparser."""
459 _ActionSearchQuery.init_subparser(parser)
460 parser.add_argument("query", help="The search query")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500461
Alex Klein1699fab2022-09-08 08:46:06 -0600462 @staticmethod
463 def __call__(opts):
464 """Implement the action."""
465 cls = FilteredQuery(opts, opts.query)
466 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400467
468
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500469class ActionMine(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600470 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500471
Alex Klein1699fab2022-09-08 08:46:06 -0600472 COMMAND = "mine"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500473
Alex Klein1699fab2022-09-08 08:46:06 -0600474 @staticmethod
475 def init_subparser(parser):
476 """Add arguments to this action's subparser."""
477 _ActionSearchQuery.init_subparser(parser)
478 parser.add_argument(
479 "--draft",
480 default=False,
481 action="store_true",
482 help="Show draft changes",
483 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500484
Alex Klein1699fab2022-09-08 08:46:06 -0600485 @staticmethod
486 def __call__(opts):
487 """Implement the action."""
488 if opts.draft:
489 rule = "is:draft"
490 else:
491 rule = "status:new"
492 cls = FilteredQuery(opts, "owner:self %s" % (rule,))
493 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700494
495
Paul Hobbs89765232015-06-24 14:07:49 -0700496def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
Alex Klein1699fab2022-09-08 08:46:06 -0600497 """Runs breadth first search starting from the nodes in |to_visit|
Paul Hobbs89765232015-06-24 14:07:49 -0700498
Alex Klein1699fab2022-09-08 08:46:06 -0600499 Args:
Trent Apted66736d82023-05-25 10:38:28 +1000500 to_visit: the starting nodes
501 children: a function which takes a node and returns the adjacent nodes
502 visited_key: a function for deduplicating node visits. Defaults to the
503 identity function (lambda x: x)
Paul Hobbs89765232015-06-24 14:07:49 -0700504
Alex Klein1699fab2022-09-08 08:46:06 -0600505 Returns:
Trent Apted66736d82023-05-25 10:38:28 +1000506 A list of nodes which are reachable from any node in |to_visit| by
507 calling
508 |children| any number of times.
Alex Klein1699fab2022-09-08 08:46:06 -0600509 """
510 to_visit = list(to_visit)
511 seen = set(visited_key(x) for x in to_visit)
512 for node in to_visit:
513 for child in children(node):
514 key = visited_key(child)
515 if key not in seen:
516 seen.add(key)
517 to_visit.append(child)
518 return to_visit
Paul Hobbs89765232015-06-24 14:07:49 -0700519
520
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500521class ActionDeps(_ActionSearchQuery):
Alex Klein4507b172023-01-13 11:39:51 -0700522 """List CLs matching a query, and transitive dependencies of those CLs."""
Paul Hobbs89765232015-06-24 14:07:49 -0700523
Alex Klein1699fab2022-09-08 08:46:06 -0600524 COMMAND = "deps"
Paul Hobbs89765232015-06-24 14:07:49 -0700525
Alex Klein1699fab2022-09-08 08:46:06 -0600526 @staticmethod
527 def init_subparser(parser):
528 """Add arguments to this action's subparser."""
529 _ActionSearchQuery.init_subparser(parser)
530 parser.add_argument("query", help="The search query")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500531
Alex Klein1699fab2022-09-08 08:46:06 -0600532 def __call__(self, opts):
533 """Implement the action."""
534 cls = _Query(opts, opts.query, raw=False)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500535
Alex Klein1699fab2022-09-08 08:46:06 -0600536 @memoize.Memoize
537 def _QueryChange(cl, helper=None):
538 return _Query(opts, cl, raw=False, helper=helper)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500539
Alex Klein1699fab2022-09-08 08:46:06 -0600540 transitives = _BreadthFirstSearch(
541 cls,
542 functools.partial(self._Children, opts, _QueryChange),
543 visited_key=lambda cl: cl.PatchLink(),
544 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500545
Alex Klein1699fab2022-09-08 08:46:06 -0600546 # This is a hack to avoid losing GoB host for each CL. The PrintCls
547 # function assumes the GoB host specified by the user is the only one
548 # that is ever used, but the deps command walks across hosts.
549 if opts.format is OutputFormat.RAW:
550 print("\n".join(x.PatchLink() for x in transitives))
551 else:
552 transitives_raw = [cl.patch_dict for cl in transitives]
553 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500554
Alex Klein1699fab2022-09-08 08:46:06 -0600555 @staticmethod
556 def _ProcessDeps(opts, querier, cl, deps, required):
557 """Yields matching dependencies for a patch"""
558 # We need to query the change to guarantee that we have a .gerrit_number
559 for dep in deps:
560 if not dep.remote in opts.gerrit:
561 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
562 remote=dep.remote, print_cmd=opts.debug
563 )
564 helper = opts.gerrit[dep.remote]
Mike Frysingerb3300c42017-07-20 01:41:17 -0400565
Alex Klein1699fab2022-09-08 08:46:06 -0600566 # TODO(phobbs) this should maybe catch network errors.
567 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400568
Alex Klein4507b172023-01-13 11:39:51 -0700569 # Handle empty results. If we found a commit that was pushed
570 # directly (e.g. a bot commit), then gerrit won't know about it.
Alex Klein1699fab2022-09-08 08:46:06 -0600571 if not changes:
572 if required:
573 logging.error(
574 "CL %s depends on %s which cannot be found",
575 cl,
576 dep.ToGerritQueryText(),
577 )
578 continue
Mike Frysinger5726da92017-09-20 22:14:25 -0400579
Alex Klein4507b172023-01-13 11:39:51 -0700580 # Our query might have matched more than one result. This can come
581 # up when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id
582 # shows up across multiple repos/branches. We blindly check all of
583 # them in the hopes that all open ones are what the user wants, but
584 # then again the CQ-DEPEND syntax itself is unable to differentiate.
585 # *shrug*
Alex Klein1699fab2022-09-08 08:46:06 -0600586 if len(changes) > 1:
587 logging.warning(
588 "CL %s has an ambiguous CQ dependency %s",
589 cl,
590 dep.ToGerritQueryText(),
591 )
592 for change in changes:
593 if change.status == "NEW":
594 yield change
Mike Frysinger5726da92017-09-20 22:14:25 -0400595
Alex Klein1699fab2022-09-08 08:46:06 -0600596 @classmethod
597 def _Children(cls, opts, querier, cl):
598 """Yields the Gerrit dependencies of a patch"""
599 for change in cls._ProcessDeps(
600 opts, querier, cl, cl.GerritDependencies(), False
601 ):
602 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700603
Paul Hobbs89765232015-06-24 14:07:49 -0700604
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500605class ActionInspect(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600606 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500607
Alex Klein1699fab2022-09-08 08:46:06 -0600608 COMMAND = "inspect"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500609
Alex Klein1699fab2022-09-08 08:46:06 -0600610 @staticmethod
611 def init_subparser(parser):
612 """Add arguments to this action's subparser."""
613 _ActionSearchQuery.init_subparser(parser)
614 parser.add_argument(
615 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
616 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500617
Alex Klein1699fab2022-09-08 08:46:06 -0600618 @staticmethod
619 def __call__(opts):
620 """Implement the action."""
621 cls = []
622 for arg in opts.cls:
623 helper, cl = GetGerrit(opts, arg)
624 change = FilteredQuery(opts, "change:%s" % cl, helper=helper)
625 if change:
626 cls.extend(change)
627 else:
628 logging.warning("no results found for CL %s", arg)
629 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400630
631
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500632class _ActionLabeler(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600633 """Base helper for setting labels."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500634
Alex Klein1699fab2022-09-08 08:46:06 -0600635 LABEL = None
636 VALUES = None
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500637
Alex Klein1699fab2022-09-08 08:46:06 -0600638 @classmethod
639 def init_subparser(cls, parser):
640 """Add arguments to this action's subparser."""
641 parser.add_argument(
642 "-m",
643 "--msg",
644 "--message",
645 metavar="MESSAGE",
646 help="Optional message to include",
647 )
648 parser.add_argument(
649 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
650 )
651 parser.add_argument(
652 "value",
653 nargs=1,
654 metavar="value",
655 choices=cls.VALUES,
656 help="The label value; one of [%(choices)s]",
657 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500658
Alex Klein1699fab2022-09-08 08:46:06 -0600659 @classmethod
660 def __call__(cls, opts):
661 """Implement the action."""
Mike Frysinger16474792023-03-01 01:18:00 -0500662
Alex Klein1699fab2022-09-08 08:46:06 -0600663 # Convert user-friendly command line option into a gerrit parameter.
664 def task(arg):
665 helper, cl = GetGerrit(opts, arg)
666 helper.SetReview(
667 cl,
668 labels={cls.LABEL: opts.value[0]},
669 msg=opts.msg,
670 dryrun=opts.dryrun,
671 notify=opts.notify,
672 )
673
Mike Frysinger5d615452023-08-21 10:51:32 -0400674 _run_parallel_tasks(task, opts.jobs, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500675
676
677class ActionLabelAutoSubmit(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600678 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500679
Alex Klein1699fab2022-09-08 08:46:06 -0600680 COMMAND = "label-as"
681 LABEL = "Auto-Submit"
682 VALUES = ("0", "1")
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600683
684
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500685class ActionLabelCodeReview(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600686 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500687
Alex Klein1699fab2022-09-08 08:46:06 -0600688 COMMAND = "label-cr"
689 LABEL = "Code-Review"
690 VALUES = ("-2", "-1", "0", "1", "2")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400691
692
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500693class ActionLabelVerified(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600694 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500695
Alex Klein1699fab2022-09-08 08:46:06 -0600696 COMMAND = "label-v"
697 LABEL = "Verified"
698 VALUES = ("-1", "0", "1")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400699
700
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500701class ActionLabelCommitQueue(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600702 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500703
Alex Klein1699fab2022-09-08 08:46:06 -0600704 COMMAND = "label-cq"
705 LABEL = "Commit-Queue"
706 VALUES = ("0", "1", "2")
707
Mike Frysinger15b23e42014-12-05 17:00:05 -0500708
C Shapiro3f1f8242021-08-02 15:28:29 -0500709class ActionLabelOwnersOverride(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600710 """Change the Owners-Override label (1=Override)"""
C Shapiro3f1f8242021-08-02 15:28:29 -0500711
Alex Klein1699fab2022-09-08 08:46:06 -0600712 COMMAND = "label-oo"
713 LABEL = "Owners-Override"
714 VALUES = ("0", "1")
C Shapiro3f1f8242021-08-02 15:28:29 -0500715
Mike Frysinger15b23e42014-12-05 17:00:05 -0500716
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500717class _ActionSimpleParallelCLs(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600718 """Base helper for actions that only accept CLs."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500719
Alex Klein1699fab2022-09-08 08:46:06 -0600720 @staticmethod
721 def init_subparser(parser):
722 """Add arguments to this action's subparser."""
723 parser.add_argument(
724 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
725 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500726
Alex Klein1699fab2022-09-08 08:46:06 -0600727 def __call__(self, opts):
728 """Implement the action."""
729
730 def task(arg):
731 helper, cl = GetGerrit(opts, arg)
732 self._process_one(helper, cl, opts)
733
Mike Frysinger5d615452023-08-21 10:51:32 -0400734 _run_parallel_tasks(task, opts.jobs, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500735
736
737class ActionSubmit(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600738 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500739
Alex Klein1699fab2022-09-08 08:46:06 -0600740 COMMAND = "submit"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500741
Alex Klein1699fab2022-09-08 08:46:06 -0600742 @staticmethod
743 def _process_one(helper, cl, opts):
744 """Use |helper| to process the single |cl|."""
745 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400746
747
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500748class ActionAbandon(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600749 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500750
Alex Klein1699fab2022-09-08 08:46:06 -0600751 COMMAND = "abandon"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500752
Alex Klein1699fab2022-09-08 08:46:06 -0600753 @staticmethod
754 def init_subparser(parser):
755 """Add arguments to this action's subparser."""
756 parser.add_argument(
757 "-m",
758 "--msg",
759 "--message",
760 metavar="MESSAGE",
761 help="Include a message",
762 )
763 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger3af378b2021-03-12 01:34:04 -0500764
Alex Klein1699fab2022-09-08 08:46:06 -0600765 @staticmethod
766 def _process_one(helper, cl, opts):
767 """Use |helper| to process the single |cl|."""
768 helper.AbandonChange(
769 cl, msg=opts.msg, dryrun=opts.dryrun, notify=opts.notify
770 )
Mike Frysinger13f23a42013-05-13 17:32:01 -0400771
772
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500773class ActionRestore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600774 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500775
Alex Klein1699fab2022-09-08 08:46:06 -0600776 COMMAND = "restore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500777
Alex Klein1699fab2022-09-08 08:46:06 -0600778 @staticmethod
779 def _process_one(helper, cl, opts):
780 """Use |helper| to process the single |cl|."""
781 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400782
783
Tomasz Figa54d70992021-01-20 13:48:59 +0900784class ActionWorkInProgress(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600785 """Mark CLs as work in progress"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900786
Alex Klein1699fab2022-09-08 08:46:06 -0600787 COMMAND = "wip"
Tomasz Figa54d70992021-01-20 13:48:59 +0900788
Alex Klein1699fab2022-09-08 08:46:06 -0600789 @staticmethod
790 def _process_one(helper, cl, opts):
791 """Use |helper| to process the single |cl|."""
792 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900793
794
795class ActionReadyForReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600796 """Mark CLs as ready for review"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900797
Alex Klein1699fab2022-09-08 08:46:06 -0600798 COMMAND = "ready"
Tomasz Figa54d70992021-01-20 13:48:59 +0900799
Alex Klein1699fab2022-09-08 08:46:06 -0600800 @staticmethod
801 def _process_one(helper, cl, opts):
802 """Use |helper| to process the single |cl|."""
803 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900804
805
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500806class ActionReviewers(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600807 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700808
Alex Klein1699fab2022-09-08 08:46:06 -0600809 COMMAND = "reviewers"
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700810
Alex Klein1699fab2022-09-08 08:46:06 -0600811 @staticmethod
812 def init_subparser(parser):
813 """Add arguments to this action's subparser."""
814 parser.add_argument("cl", metavar="CL", help="The CL to update")
815 parser.add_argument(
Mike Frysinger1a66eda2023-08-21 11:18:47 -0400816 "reviewers",
817 type="email",
818 nargs="+",
819 help="The reviewers to add/remove",
Alex Klein1699fab2022-09-08 08:46:06 -0600820 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700821
Alex Klein1699fab2022-09-08 08:46:06 -0600822 @staticmethod
823 def __call__(opts):
824 """Implement the action."""
Mike Frysinger1a66eda2023-08-21 11:18:47 -0400825 add_list, remove_list = process_add_remove_lists(opts.reviewers)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500826
Alex Klein1699fab2022-09-08 08:46:06 -0600827 if add_list or remove_list:
828 helper, cl = GetGerrit(opts, opts.cl)
829 helper.SetReviewers(
830 cl,
831 add=add_list,
832 remove=remove_list,
833 dryrun=opts.dryrun,
834 notify=opts.notify,
835 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700836
837
Brian Norrisd25af082021-10-29 11:25:31 -0700838class ActionAttentionSet(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600839 """Add/remove emails from the attention set (prepend with '~' to remove)"""
Brian Norrisd25af082021-10-29 11:25:31 -0700840
Alex Klein1699fab2022-09-08 08:46:06 -0600841 COMMAND = "attention"
Brian Norrisd25af082021-10-29 11:25:31 -0700842
Alex Klein1699fab2022-09-08 08:46:06 -0600843 @staticmethod
844 def init_subparser(parser):
845 """Add arguments to this action's subparser."""
846 parser.add_argument(
847 "-m",
848 "--msg",
849 "--message",
850 metavar="MESSAGE",
851 help="Optional message to include",
852 default="gerrit CLI",
853 )
854 parser.add_argument("cl", metavar="CL", help="The CL to update")
855 parser.add_argument(
856 "users",
Mike Frysinger1a66eda2023-08-21 11:18:47 -0400857 type="email",
Alex Klein1699fab2022-09-08 08:46:06 -0600858 nargs="+",
859 help="The users to add/remove from attention set",
860 )
Brian Norrisd25af082021-10-29 11:25:31 -0700861
Alex Klein1699fab2022-09-08 08:46:06 -0600862 @staticmethod
863 def __call__(opts):
864 """Implement the action."""
Mike Frysinger1a66eda2023-08-21 11:18:47 -0400865 add_list, remove_list = process_add_remove_lists(opts.users)
Brian Norrisd25af082021-10-29 11:25:31 -0700866
Alex Klein1699fab2022-09-08 08:46:06 -0600867 if add_list or remove_list:
868 helper, cl = GetGerrit(opts, opts.cl)
869 helper.SetAttentionSet(
870 cl,
871 add=add_list,
872 remove=remove_list,
873 dryrun=opts.dryrun,
874 notify=opts.notify,
875 message=opts.msg,
876 )
Brian Norrisd25af082021-10-29 11:25:31 -0700877
878
Mike Frysinger62178ae2020-03-20 01:37:43 -0400879class ActionMessage(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600880 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500881
Alex Klein1699fab2022-09-08 08:46:06 -0600882 COMMAND = "message"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500883
Alex Klein1699fab2022-09-08 08:46:06 -0600884 @staticmethod
885 def init_subparser(parser):
886 """Add arguments to this action's subparser."""
887 _ActionSimpleParallelCLs.init_subparser(parser)
888 parser.add_argument("message", help="The message to post")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500889
Alex Klein1699fab2022-09-08 08:46:06 -0600890 @staticmethod
891 def _process_one(helper, cl, opts):
892 """Use |helper| to process the single |cl|."""
893 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530894
895
Mike Frysinger62178ae2020-03-20 01:37:43 -0400896class ActionTopic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600897 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500898
Alex Klein1699fab2022-09-08 08:46:06 -0600899 COMMAND = "topic"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500900
Alex Klein1699fab2022-09-08 08:46:06 -0600901 @staticmethod
902 def init_subparser(parser):
903 """Add arguments to this action's subparser."""
904 _ActionSimpleParallelCLs.init_subparser(parser)
905 parser.add_argument("topic", help="The topic to set")
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.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800911
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800912
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500913class ActionPrivate(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600914 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700915
Alex Klein1699fab2022-09-08 08:46:06 -0600916 COMMAND = "private"
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, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700922
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800923
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500924class ActionPublic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600925 """Mark CLs public"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500926
Alex Klein1699fab2022-09-08 08:46:06 -0600927 COMMAND = "public"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500928
Alex Klein1699fab2022-09-08 08:46:06 -0600929 @staticmethod
930 def _process_one(helper, cl, opts):
931 """Use |helper| to process the single |cl|."""
932 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500933
934
935class ActionSethashtags(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600936 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500937
Alex Klein1699fab2022-09-08 08:46:06 -0600938 COMMAND = "hashtags"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500939
Alex Klein1699fab2022-09-08 08:46:06 -0600940 @staticmethod
941 def init_subparser(parser):
942 """Add arguments to this action's subparser."""
943 parser.add_argument("cl", metavar="CL", help="The CL to update")
944 parser.add_argument(
945 "hashtags", nargs="+", help="The hashtags to add/remove"
946 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500947
Alex Klein1699fab2022-09-08 08:46:06 -0600948 @staticmethod
949 def __call__(opts):
950 """Implement the action."""
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400951 add, remove = process_add_remove_lists(opts.hashtags)
Alex Klein1699fab2022-09-08 08:46:06 -0600952 helper, cl = GetGerrit(opts, opts.cl)
Cheng Yuehb0ed9462023-08-14 14:06:33 +0800953 helper.SetHashtags(cl, list(add), list(remove), dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800954
955
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400956class ActionDelete(_ActionSimpleParallelCLs):
957 """Delete CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500958
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400959 COMMAND = "delete"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500960
Alex Klein1699fab2022-09-08 08:46:06 -0600961 @staticmethod
962 def _process_one(helper, cl, opts):
963 """Use |helper| to process the single |cl|."""
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400964 helper.Delete(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800965
966
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500967class ActionReviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600968 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500969
Alex Klein1699fab2022-09-08 08:46:06 -0600970 COMMAND = "reviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500971
Alex Klein1699fab2022-09-08 08:46:06 -0600972 @staticmethod
973 def _process_one(helper, cl, opts):
974 """Use |helper| to process the single |cl|."""
975 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500976
977
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500978class ActionUnreviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600979 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500980
Alex Klein1699fab2022-09-08 08:46:06 -0600981 COMMAND = "unreviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500982
Alex Klein1699fab2022-09-08 08:46:06 -0600983 @staticmethod
984 def _process_one(helper, cl, opts):
985 """Use |helper| to process the single |cl|."""
986 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500987
988
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500989class ActionIgnore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600990 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500991
Alex Klein1699fab2022-09-08 08:46:06 -0600992 COMMAND = "ignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500993
Alex Klein1699fab2022-09-08 08:46:06 -0600994 @staticmethod
995 def _process_one(helper, cl, opts):
996 """Use |helper| to process the single |cl|."""
997 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500998
999
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001000class ActionUnignore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001001 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001002
Alex Klein1699fab2022-09-08 08:46:06 -06001003 COMMAND = "unignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001004
Alex Klein1699fab2022-09-08 08:46:06 -06001005 @staticmethod
1006 def _process_one(helper, cl, opts):
1007 """Use |helper| to process the single |cl|."""
1008 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -05001009
1010
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001011class ActionCherryPick(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001012 """Cherry-pick CLs to branches."""
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001013
Alex Klein1699fab2022-09-08 08:46:06 -06001014 COMMAND = "cherry-pick"
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001015
Alex Klein1699fab2022-09-08 08:46:06 -06001016 @staticmethod
1017 def init_subparser(parser):
1018 """Add arguments to this action's subparser."""
1019 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
1020 parser.add_argument(
1021 "--rev",
1022 "--revision",
1023 default="current",
1024 help="A specific revision or patchset",
1025 )
1026 parser.add_argument(
1027 "-m",
1028 "--msg",
1029 "--message",
1030 metavar="MESSAGE",
1031 help="Include a message",
1032 )
1033 parser.add_argument(
1034 "--branches",
1035 "--branch",
1036 "--br",
1037 action="split_extend",
1038 default=[],
1039 required=True,
1040 help="The destination branches",
1041 )
1042 parser.add_argument(
Mike Frysingera8adc4e2023-08-21 13:13:15 -04001043 "--allow-conflicts",
1044 action="store_true",
1045 help="Cherry-pick the CL with conflicts.",
1046 )
1047 parser.add_argument(
Alex Klein1699fab2022-09-08 08:46:06 -06001048 "cls", nargs="+", metavar="CL", help="The CLs to cherry-pick"
1049 )
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001050
Alex Klein1699fab2022-09-08 08:46:06 -06001051 @staticmethod
1052 def __call__(opts):
1053 """Implement the action."""
Mike Frysinger16474792023-03-01 01:18:00 -05001054
Alex Klein1699fab2022-09-08 08:46:06 -06001055 # Process branches in parallel, but CLs in serial in case of CL stacks.
1056 def task(branch):
1057 for arg in opts.cls:
1058 helper, cl = GetGerrit(opts, arg)
1059 ret = helper.CherryPick(
1060 cl,
1061 branch,
1062 rev=opts.rev,
1063 msg=opts.msg,
Mike Frysingera8adc4e2023-08-21 13:13:15 -04001064 allow_conflicts=opts.allow_conflicts,
Alex Klein1699fab2022-09-08 08:46:06 -06001065 dryrun=opts.dryrun,
1066 notify=opts.notify,
1067 )
1068 logging.debug("Response: %s", ret)
1069 if opts.format is OutputFormat.RAW:
1070 print(ret["_number"])
1071 else:
1072 uri = f'https://{helper.host}/c/{ret["_number"]}'
1073 print(uri_lib.ShortenUri(uri))
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001074
Mike Frysinger5d615452023-08-21 10:51:32 -04001075 _run_parallel_tasks(task, opts.jobs, *opts.branches)
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001076
1077
Mike Frysinger8037f752020-02-29 20:47:09 -05001078class ActionReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001079 """Review CLs with multiple settings
Mike Frysinger8037f752020-02-29 20:47:09 -05001080
Mike Frysingerd74a8bc2023-06-22 09:42:55 -04001081 The reviewers & cc options can remove people by prepending '~' or '-'.
1082 Note: If you want to move someone (reviewer->CC or CC->reviewer), you don't
1083 have to remove them first, you only need to specify the final state.
Mike Frysingere5a69832023-06-22 09:34:57 -04001084
Alex Klein4507b172023-01-13 11:39:51 -07001085 The label option supports extended/multiple syntax for easy use. The --label
1086 option may be specified multiple times (as settings are merges), and
1087 multiple labels are allowed in a single argument. Each label has the form:
Alex Klein1699fab2022-09-08 08:46:06 -06001088 <long or short name><=+-><value>
Mike Frysinger8037f752020-02-29 20:47:09 -05001089
Alex Klein1699fab2022-09-08 08:46:06 -06001090 Common arguments:
1091 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
1092 'V+1 CQ+2'
1093 'AS=1 V=1'
1094 """
Mike Frysinger8037f752020-02-29 20:47:09 -05001095
Alex Klein1699fab2022-09-08 08:46:06 -06001096 COMMAND = "review"
Mike Frysinger8037f752020-02-29 20:47:09 -05001097
Alex Klein1699fab2022-09-08 08:46:06 -06001098 class _SetLabel(argparse.Action):
1099 """Argparse action for setting labels."""
Mike Frysinger8037f752020-02-29 20:47:09 -05001100
Alex Klein1699fab2022-09-08 08:46:06 -06001101 LABEL_MAP = {
1102 "AS": "Auto-Submit",
1103 "CQ": "Commit-Queue",
1104 "CR": "Code-Review",
1105 "V": "Verified",
1106 }
Mike Frysinger8037f752020-02-29 20:47:09 -05001107
Alex Klein1699fab2022-09-08 08:46:06 -06001108 def __call__(self, parser, namespace, values, option_string=None):
1109 labels = getattr(namespace, self.dest)
1110 for request in values.split():
1111 if "=" in request:
1112 # Handle Verified=1 form.
1113 short, value = request.split("=", 1)
1114 elif "+" in request:
1115 # Handle Verified+1 form.
1116 short, value = request.split("+", 1)
1117 elif "-" in request:
1118 # Handle Verified-1 form.
1119 short, value = request.split("-", 1)
1120 value = "-%s" % (value,)
1121 else:
1122 parser.error(
1123 'Invalid label setting "%s". Must be Commit-Queue=1 or '
1124 "CQ+1 or CR-1." % (request,)
1125 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001126
Alex Klein1699fab2022-09-08 08:46:06 -06001127 # Convert possible short label names like "V" to "Verified".
1128 label = self.LABEL_MAP.get(short)
1129 if not label:
1130 label = short
Mike Frysinger8037f752020-02-29 20:47:09 -05001131
Alex Klein1699fab2022-09-08 08:46:06 -06001132 # We allow existing label requests to be overridden.
1133 labels[label] = value
Mike Frysinger8037f752020-02-29 20:47:09 -05001134
Alex Klein1699fab2022-09-08 08:46:06 -06001135 @classmethod
1136 def init_subparser(cls, parser):
1137 """Add arguments to this action's subparser."""
1138 parser.add_argument(
1139 "-m",
1140 "--msg",
1141 "--message",
1142 metavar="MESSAGE",
1143 help="Include a message",
1144 )
1145 parser.add_argument(
1146 "-l",
1147 "--label",
1148 dest="labels",
1149 action=cls._SetLabel,
1150 default={},
1151 help="Set a label with a value",
1152 )
1153 parser.add_argument(
1154 "--ready",
1155 default=None,
1156 action="store_true",
1157 help="Set CL status to ready-for-review",
1158 )
1159 parser.add_argument(
1160 "--wip",
1161 default=None,
1162 action="store_true",
1163 help="Set CL status to WIP",
1164 )
1165 parser.add_argument(
1166 "--reviewers",
1167 "--re",
1168 action="append",
Mike Frysinger1a66eda2023-08-21 11:18:47 -04001169 type="email",
Alex Klein1699fab2022-09-08 08:46:06 -06001170 default=[],
Mike Frysingere5a69832023-06-22 09:34:57 -04001171 help="Reviewers to add/remove",
Alex Klein1699fab2022-09-08 08:46:06 -06001172 )
1173 parser.add_argument(
Mike Frysingere5a69832023-06-22 09:34:57 -04001174 "--cc",
1175 action="append",
Mike Frysinger1a66eda2023-08-21 11:18:47 -04001176 type="email",
Mike Frysingere5a69832023-06-22 09:34:57 -04001177 default=[],
1178 help="People to add/remove in CC",
Alex Klein1699fab2022-09-08 08:46:06 -06001179 )
1180 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger8037f752020-02-29 20:47:09 -05001181
Alex Klein1699fab2022-09-08 08:46:06 -06001182 @staticmethod
1183 def _process_one(helper, cl, opts):
1184 """Use |helper| to process the single |cl|."""
Mike Frysingere5a69832023-06-22 09:34:57 -04001185 add_reviewers, remove_reviewers = process_add_remove_lists(
Mike Frysinger1a66eda2023-08-21 11:18:47 -04001186 opts.reviewers
Mike Frysingere5a69832023-06-22 09:34:57 -04001187 )
Mike Frysinger1a66eda2023-08-21 11:18:47 -04001188 add_cc, remove_cc = process_add_remove_lists(opts.cc)
Mike Frysingere5a69832023-06-22 09:34:57 -04001189
1190 # Gerrit allows people to only be in one state: CC or Reviewer. If a
1191 # person is in CC and you want to move them to reviewer, you can't
1192 # remove them from CC and add to reviewer, you have to change their
1193 # state. Help users who do `--cc ~u@c --re u@c` by filtering out all
1194 # the remove requests if there is an add request too. This doesn't
1195 # quite respect all the possible CLI option orders, but it's probably
1196 # good enough for now in practice. For example, mixing of CC & reviewer
1197 # and adds & removes gets complicated.
1198 for add in add_cc:
1199 if add in remove_reviewers:
1200 remove_reviewers.remove(add)
1201 for add in add_reviewers:
1202 if add in remove_cc:
1203 remove_cc.remove(add)
1204
Alex Klein1699fab2022-09-08 08:46:06 -06001205 helper.SetReview(
1206 cl,
1207 msg=opts.msg,
1208 labels=opts.labels,
1209 dryrun=opts.dryrun,
1210 notify=opts.notify,
Mike Frysingere5a69832023-06-22 09:34:57 -04001211 reviewers=add_reviewers,
1212 cc=add_cc,
1213 remove_reviewers=remove_reviewers | remove_cc,
Alex Klein1699fab2022-09-08 08:46:06 -06001214 ready=opts.ready,
1215 wip=opts.wip,
1216 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001217
1218
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001219class ActionAccount(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001220 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001221
Alex Klein1699fab2022-09-08 08:46:06 -06001222 COMMAND = "account"
1223 USE_PAGER = True
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001224
Alex Klein1699fab2022-09-08 08:46:06 -06001225 @staticmethod
1226 def init_subparser(parser):
1227 """Add arguments to this action's subparser."""
1228 parser.add_argument(
1229 "accounts",
1230 nargs="*",
1231 default=["self"],
1232 help="The accounts to query",
1233 )
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001234
Alex Klein1699fab2022-09-08 08:46:06 -06001235 @classmethod
1236 def __call__(cls, opts):
1237 """Implement the action."""
1238 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001239
Alex Klein1699fab2022-09-08 08:46:06 -06001240 def print_one(header, data):
1241 print(f"### {header}")
1242 compact = opts.format is OutputFormat.JSON
1243 print(pformat.json(data, compact=compact).rstrip())
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001244
Alex Klein1699fab2022-09-08 08:46:06 -06001245 def task(arg):
1246 detail = gob_util.FetchUrlJson(
1247 helper.host, f"accounts/{arg}/detail"
1248 )
1249 if not detail:
1250 print(f"{arg}: account not found")
1251 else:
1252 print_one("detail", detail)
1253 for field in (
Mike Frysinger03c75072023-03-10 17:25:21 -05001254 "external.ids",
Alex Klein1699fab2022-09-08 08:46:06 -06001255 "groups",
1256 "capabilities",
1257 "preferences",
1258 "sshkeys",
1259 "gpgkeys",
1260 ):
1261 data = gob_util.FetchUrlJson(
1262 helper.host, f"accounts/{arg}/{field}"
1263 )
1264 print_one(field, data)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001265
Mike Frysinger5d615452023-08-21 10:51:32 -04001266 _run_parallel_tasks(task, opts.jobs, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -08001267
1268
Mike Frysinger2295d792021-03-08 15:55:23 -05001269class ActionConfig(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001270 """Manage the gerrit tool's own config file
Mike Frysinger2295d792021-03-08 15:55:23 -05001271
Alex Klein1699fab2022-09-08 08:46:06 -06001272 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
1273 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
Mike Frysinger2295d792021-03-08 15:55:23 -05001274
Alex Klein1699fab2022-09-08 08:46:06 -06001275 # Set up subcommand aliases.
1276 [alias]
1277 common-search = search 'is:open project:something/i/care/about'
1278 """
Mike Frysinger2295d792021-03-08 15:55:23 -05001279
Alex Klein1699fab2022-09-08 08:46:06 -06001280 COMMAND = "config"
Mike Frysinger2295d792021-03-08 15:55:23 -05001281
Alex Klein1699fab2022-09-08 08:46:06 -06001282 @staticmethod
1283 def __call__(opts):
1284 """Implement the action."""
Alex Klein4507b172023-01-13 11:39:51 -07001285 # For now, this is a place holder for raising visibility for the config
1286 # file and its associated help text documentation.
Alex Klein1699fab2022-09-08 08:46:06 -06001287 opts.parser.parse_args(["config", "--help"])
Mike Frysinger2295d792021-03-08 15:55:23 -05001288
1289
Mike Frysingere5450602021-03-08 15:34:17 -05001290class ActionHelp(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001291 """An alias to --help for CLI symmetry"""
Mike Frysingere5450602021-03-08 15:34:17 -05001292
Alex Klein1699fab2022-09-08 08:46:06 -06001293 COMMAND = "help"
1294 USE_PAGER = True
Mike Frysingere5450602021-03-08 15:34:17 -05001295
Alex Klein1699fab2022-09-08 08:46:06 -06001296 @staticmethod
1297 def init_subparser(parser):
1298 """Add arguments to this action's subparser."""
1299 parser.add_argument(
1300 "command", nargs="?", help="The command to display."
1301 )
Mike Frysingere5450602021-03-08 15:34:17 -05001302
Alex Klein1699fab2022-09-08 08:46:06 -06001303 @staticmethod
1304 def __call__(opts):
1305 """Implement the action."""
1306 # Show global help.
1307 if not opts.command:
1308 opts.parser.print_help()
1309 return
Mike Frysingere5450602021-03-08 15:34:17 -05001310
Alex Klein1699fab2022-09-08 08:46:06 -06001311 opts.parser.parse_args([opts.command, "--help"])
Mike Frysingere5450602021-03-08 15:34:17 -05001312
1313
Mike Frysinger484e2f82020-03-20 01:41:10 -04001314class ActionHelpAll(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001315 """Show all actions help output at once."""
Mike Frysinger484e2f82020-03-20 01:41:10 -04001316
Alex Klein1699fab2022-09-08 08:46:06 -06001317 COMMAND = "help-all"
1318 USE_PAGER = True
Mike Frysinger484e2f82020-03-20 01:41:10 -04001319
Alex Klein1699fab2022-09-08 08:46:06 -06001320 @staticmethod
1321 def __call__(opts):
1322 """Implement the action."""
1323 first = True
1324 for action in _GetActions():
1325 if first:
1326 first = False
1327 else:
1328 print("\n\n")
Mike Frysinger484e2f82020-03-20 01:41:10 -04001329
Alex Klein1699fab2022-09-08 08:46:06 -06001330 try:
1331 opts.parser.parse_args([action, "--help"])
1332 except SystemExit:
1333 pass
Mike Frysinger484e2f82020-03-20 01:41:10 -04001334
1335
Mike Frysinger65fc8632020-02-06 18:11:12 -05001336@memoize.Memoize
1337def _GetActions():
Alex Klein1699fab2022-09-08 08:46:06 -06001338 """Get all the possible actions we support.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001339
Alex Klein1699fab2022-09-08 08:46:06 -06001340 Returns:
Trent Apted66736d82023-05-25 10:38:28 +10001341 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1342 function that implements that command (e.g. UserActFoo).
Alex Klein1699fab2022-09-08 08:46:06 -06001343 """
1344 VALID_NAME = re.compile(r"^[a-z][a-z-]*[a-z]$")
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001345
Alex Klein1699fab2022-09-08 08:46:06 -06001346 actions = {}
1347 for cls in globals().values():
1348 if (
1349 not inspect.isclass(cls)
1350 or not issubclass(cls, UserAction)
1351 or not getattr(cls, "COMMAND", None)
1352 ):
1353 continue
Mike Frysinger65fc8632020-02-06 18:11:12 -05001354
Alex Klein1699fab2022-09-08 08:46:06 -06001355 # Sanity check names for devs adding new commands. Should be quick.
1356 cmd = cls.COMMAND
1357 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1358 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001359
Alex Klein1699fab2022-09-08 08:46:06 -06001360 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001361
Alex Klein1699fab2022-09-08 08:46:06 -06001362 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001363
1364
Harry Cutts26076b32019-02-26 15:01:29 -08001365def _GetActionUsages():
Alex Klein1699fab2022-09-08 08:46:06 -06001366 """Formats a one-line usage and doc message for each action."""
1367 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001368
Alex Klein1699fab2022-09-08 08:46:06 -06001369 cmds = list(actions.keys())
1370 functions = list(actions.values())
1371 usages = [getattr(x, "usage", "") for x in functions]
1372 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001373
Alex Klein1699fab2022-09-08 08:46:06 -06001374 cmd_indent = len(max(cmds, key=len))
1375 usage_indent = len(max(usages, key=len))
1376 return "\n".join(
1377 " %-*s %-*s : %s" % (cmd_indent, cmd, usage_indent, usage, doc)
1378 for cmd, usage, doc in zip(cmds, usages, docs)
1379 )
Harry Cutts26076b32019-02-26 15:01:29 -08001380
1381
Mike Frysinger2295d792021-03-08 15:55:23 -05001382def _AddCommonOptions(parser, subparser):
Alex Klein1699fab2022-09-08 08:46:06 -06001383 """Add options that should work before & after the subcommand.
Mike Frysinger2295d792021-03-08 15:55:23 -05001384
Alex Klein1699fab2022-09-08 08:46:06 -06001385 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1386 """
1387 parser.add_common_argument_to_group(
1388 subparser,
1389 "--ne",
1390 "--no-emails",
1391 dest="notify",
1392 default="ALL",
1393 action="store_const",
1394 const="NONE",
1395 help="Do not send e-mail notifications",
1396 )
1397 parser.add_common_argument_to_group(
1398 subparser,
1399 "-n",
1400 "--dry-run",
1401 dest="dryrun",
1402 default=False,
1403 action="store_true",
1404 help="Show what would be done, but do not make changes",
1405 )
Mike Frysinger5d615452023-08-21 10:51:32 -04001406 parser.add_common_argument_to_group(
1407 subparser,
1408 "-j",
1409 "--jobs",
1410 type=int,
1411 default=CONNECTION_LIMIT,
1412 help=(
1413 "Number of connections to run in parallel. "
1414 f"(default: {CONNECTION_LIMIT})"
1415 ),
1416 )
Mike Frysinger2295d792021-03-08 15:55:23 -05001417
1418
1419def GetBaseParser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001420 """Returns the common parser (i.e. no subparsers added)."""
1421 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001422There is no support for doing line-by-line code review via the command line.
1423This helps you manage various bits and CL status.
1424
Mike Frysingera1db2c42014-06-15 00:42:48 -07001425For general Gerrit documentation, see:
1426 https://gerrit-review.googlesource.com/Documentation/
1427The Searching Changes page covers the search query syntax:
1428 https://gerrit-review.googlesource.com/Documentation/user-search.html
1429
Mike Frysinger13f23a42013-05-13 17:32:01 -04001430Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001431 $ gerrit todo # List all the CLs that await your review.
1432 $ gerrit mine # List all of your open CLs.
1433 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1434 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1435 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001436 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
143728123.
1438 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1439CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001440Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001441 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1442with Commit-Queue=1.
1443 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1444CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001445 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001446
Harry Cutts26076b32019-02-26 15:01:29 -08001447Actions:
1448"""
Alex Klein1699fab2022-09-08 08:46:06 -06001449 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001450
Alex Klein1699fab2022-09-08 08:46:06 -06001451 site_params = config_lib.GetSiteParams()
1452 parser = commandline.ArgumentParser(
1453 description=description,
1454 default_log_level="notice",
1455 epilog="For subcommand help, use `gerrit help <command>`.",
1456 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001457
Alex Klein1699fab2022-09-08 08:46:06 -06001458 group = parser.add_argument_group("Server options")
1459 group.add_argument(
1460 "-i",
1461 "--internal",
1462 dest="gob",
1463 action="store_const",
1464 default=site_params.EXTERNAL_GOB_INSTANCE,
1465 const=site_params.INTERNAL_GOB_INSTANCE,
1466 help="Query internal Chrome Gerrit instance",
1467 )
1468 group.add_argument(
1469 "-g",
1470 "--gob",
1471 default=site_params.EXTERNAL_GOB_INSTANCE,
Trent Apted66736d82023-05-25 10:38:28 +10001472 help="Gerrit (on borg) instance to query (default: %(default)s)",
Alex Klein1699fab2022-09-08 08:46:06 -06001473 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001474
Alex Klein1699fab2022-09-08 08:46:06 -06001475 group = parser.add_argument_group("CL options")
1476 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001477
Alex Klein1699fab2022-09-08 08:46:06 -06001478 group = parser.add_mutually_exclusive_group()
1479 parser.set_defaults(format=OutputFormat.AUTO)
1480 group.add_argument(
1481 "--format",
1482 action="enum",
1483 enum=OutputFormat,
1484 help="Output format to use.",
1485 )
1486 group.add_argument(
1487 "--raw",
1488 action="store_const",
1489 dest="format",
1490 const=OutputFormat.RAW,
1491 help="Alias for --format=raw.",
1492 )
1493 group.add_argument(
1494 "--json",
1495 action="store_const",
1496 dest="format",
1497 const=OutputFormat.JSON,
1498 help="Alias for --format=json.",
1499 )
Jack Rosenthal95aac172022-06-30 15:35:07 -06001500
Alex Klein1699fab2022-09-08 08:46:06 -06001501 group = parser.add_mutually_exclusive_group()
1502 group.add_argument(
1503 "--pager",
1504 action="store_true",
1505 default=sys.stdout.isatty(),
1506 help="Enable pager.",
1507 )
1508 group.add_argument(
1509 "--no-pager", action="store_false", dest="pager", help="Disable pager."
1510 )
1511 return parser
Mike Frysinger2295d792021-03-08 15:55:23 -05001512
1513
Alex Klein1699fab2022-09-08 08:46:06 -06001514def GetParser(
1515 parser: commandline.ArgumentParser = None,
Mike Frysinger16474792023-03-01 01:18:00 -05001516) -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001517 """Returns the full parser to use for this module."""
1518 if parser is None:
1519 parser = GetBaseParser()
Mike Frysinger2295d792021-03-08 15:55:23 -05001520
Alex Klein1699fab2022-09-08 08:46:06 -06001521 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001522
Alex Klein1699fab2022-09-08 08:46:06 -06001523 # Subparsers are required by default under Python 2. Python 3 changed to
1524 # not required, but didn't include a required option until 3.7. Setting
1525 # the required member works in all versions (and setting dest name).
1526 subparsers = parser.add_subparsers(dest="action")
1527 subparsers.required = True
1528 for cmd, cls in actions.items():
1529 # Format the full docstring by removing the file level indentation.
1530 description = re.sub(r"^ ", "", cls.__doc__, flags=re.M)
1531 subparser = subparsers.add_parser(cmd, description=description)
1532 _AddCommonOptions(parser, subparser)
1533 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001534
Alex Klein1699fab2022-09-08 08:46:06 -06001535 return parser
Mike Frysinger108eda22018-06-06 18:45:12 -04001536
1537
Jack Rosenthal95aac172022-06-30 15:35:07 -06001538def start_pager():
Alex Klein1699fab2022-09-08 08:46:06 -06001539 """Re-spawn ourselves attached to a pager."""
1540 pager = os.environ.get("PAGER", "less")
1541 os.environ.setdefault("LESS", "FRX")
Jack Rosenthal95aac172022-06-30 15:35:07 -06001542 with subprocess.Popen(
Alex Klein1699fab2022-09-08 08:46:06 -06001543 # sys.argv can have some edge cases: we may not necessarily use
1544 # sys.executable if the script is executed as "python path/to/script".
1545 # If we upgrade to Python 3.10+, this should be changed to sys.orig_argv
1546 # for full accuracy.
1547 sys.argv,
1548 stdout=subprocess.PIPE,
1549 stderr=subprocess.STDOUT,
1550 env={"GERRIT_RESPAWN_FOR_PAGER": "1", **os.environ},
1551 ) as gerrit_proc:
1552 with subprocess.Popen(
1553 pager,
1554 shell=True,
1555 stdin=gerrit_proc.stdout,
1556 ) as pager_proc:
1557 # Send SIGINT to just the gerrit process, not the pager too.
1558 def _sighandler(signum, _frame):
1559 gerrit_proc.send_signal(signum)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001560
Alex Klein1699fab2022-09-08 08:46:06 -06001561 signal.signal(signal.SIGINT, _sighandler)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001562
Alex Klein1699fab2022-09-08 08:46:06 -06001563 pager_proc.communicate()
1564 # If the pager exits, and the gerrit process is still running, we
1565 # must terminate it.
1566 if gerrit_proc.poll() is None:
1567 gerrit_proc.terminate()
1568 sys.exit(gerrit_proc.wait())
Jack Rosenthal95aac172022-06-30 15:35:07 -06001569
1570
Mike Frysinger108eda22018-06-06 18:45:12 -04001571def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -06001572 base_parser = GetBaseParser()
1573 opts, subargs = base_parser.parse_known_args(argv)
Mike Frysinger2295d792021-03-08 15:55:23 -05001574
Alex Klein1699fab2022-09-08 08:46:06 -06001575 config = Config()
1576 if subargs:
Alex Klein4507b172023-01-13 11:39:51 -07001577 # If the action is an alias to an expanded value, we need to mutate the
1578 # argv and reparse things.
Alex Klein1699fab2022-09-08 08:46:06 -06001579 action = config.expand_alias(subargs[0])
1580 if action != subargs[0]:
1581 pos = argv.index(subargs[0])
1582 argv = argv[:pos] + action + argv[pos + 1 :]
Mike Frysinger2295d792021-03-08 15:55:23 -05001583
Alex Klein1699fab2022-09-08 08:46:06 -06001584 parser = GetParser(parser=base_parser)
1585 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001586
Alex Klein1699fab2022-09-08 08:46:06 -06001587 # If we're running as a re-spawn for the pager, from this point on
1588 # we'll pretend we're attached to a TTY. This will give us colored
1589 # output when requested.
1590 if os.environ.pop("GERRIT_RESPAWN_FOR_PAGER", None) is not None:
1591 opts.pager = False
1592 sys.stdout.isatty = lambda: True
Jack Rosenthal95aac172022-06-30 15:35:07 -06001593
Alex Klein1699fab2022-09-08 08:46:06 -06001594 # In case the action wants to throw a parser error.
1595 opts.parser = parser
Mike Frysinger484e2f82020-03-20 01:41:10 -04001596
Alex Klein1699fab2022-09-08 08:46:06 -06001597 # A cache of gerrit helpers we'll load on demand.
1598 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001599
Alex Klein1699fab2022-09-08 08:46:06 -06001600 if opts.format is OutputFormat.AUTO:
1601 if sys.stdout.isatty():
1602 opts.format = OutputFormat.PRETTY
1603 else:
1604 opts.format = OutputFormat.RAW
Jack Rosenthale3a92672022-06-29 14:54:48 -06001605
Alex Klein1699fab2022-09-08 08:46:06 -06001606 opts.Freeze()
Mike Frysinger88f27292014-06-17 09:40:45 -07001607
Alex Klein1699fab2022-09-08 08:46:06 -06001608 # pylint: disable=global-statement
1609 global COLOR
1610 COLOR = terminal.Color(enabled=opts.color)
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001611
Alex Klein1699fab2022-09-08 08:46:06 -06001612 # Now look up the requested user action and run it.
1613 actions = _GetActions()
1614 action_class = actions[opts.action]
1615 if action_class.USE_PAGER and opts.pager:
1616 start_pager()
1617 obj = action_class()
1618 try:
1619 obj(opts)
1620 except (
1621 cros_build_lib.RunCommandError,
1622 gerrit.GerritException,
1623 gob_util.GOBError,
1624 ) as e:
1625 cros_build_lib.Die(e)