blob: 9fbe03ba5898c9a696e2149351e6ba11b3df3bae [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 Frysinger32c1d9f2023-06-22 09:07:19 -040027from typing import List, Optional, 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
32from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040033from chromite.lib import cros_build_lib
34from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050035from chromite.lib import gob_util
Mike Frysinger254f33f2019-12-11 13:54:29 -050036from chromite.lib import parallel
Mike Frysingera9751c92021-04-30 10:12:37 -040037from chromite.lib import retry_util
Mike Frysinger13f23a42013-05-13 17:32:01 -040038from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040039from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060040from chromite.utils import memoize
Alex Klein73eba212021-09-09 11:43:33 -060041from chromite.utils import pformat
Mike Frysinger13f23a42013-05-13 17:32:01 -040042
43
Mike Frysinger2295d792021-03-08 15:55:23 -050044class Config:
Alex Klein1699fab2022-09-08 08:46:06 -060045 """Manage the user's gerrit config settings.
Mike Frysinger2295d792021-03-08 15:55:23 -050046
Alex Klein1699fab2022-09-08 08:46:06 -060047 This is entirely unique to this gerrit command. Inspiration for naming and
48 layout is taken from ~/.gitconfig settings.
49 """
Mike Frysinger2295d792021-03-08 15:55:23 -050050
Alex Klein1699fab2022-09-08 08:46:06 -060051 def __init__(self, path: Path = chromite_config.GERRIT_CONFIG):
52 self.cfg = configparser.ConfigParser(interpolation=None)
53 if path.exists():
54 self.cfg.read(chromite_config.GERRIT_CONFIG)
Mike Frysinger2295d792021-03-08 15:55:23 -050055
Alex Klein1699fab2022-09-08 08:46:06 -060056 def expand_alias(self, action):
57 """Expand any aliases."""
58 alias = self.cfg.get("alias", action, fallback=None)
59 if alias is not None:
60 return shlex.split(alias)
61 return action
Mike Frysinger2295d792021-03-08 15:55:23 -050062
63
Alex Klein074f94f2023-06-22 10:32:06 -060064class UserAction:
Alex Klein1699fab2022-09-08 08:46:06 -060065 """Base class for all custom user actions."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -050066
Alex Klein1699fab2022-09-08 08:46:06 -060067 # The name of the command the user types in.
68 COMMAND = None
Mike Frysingerc7796cf2020-02-06 23:55:15 -050069
Alex Klein1699fab2022-09-08 08:46:06 -060070 # Should output be paged?
71 USE_PAGER = False
Jack Rosenthal95aac172022-06-30 15:35:07 -060072
Alex Klein1699fab2022-09-08 08:46:06 -060073 @staticmethod
74 def init_subparser(parser):
75 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -050076
Alex Klein1699fab2022-09-08 08:46:06 -060077 @staticmethod
78 def __call__(opts):
79 """Implement the action."""
80 raise RuntimeError(
81 "Internal error: action missing __call__ implementation"
82 )
Mike Frysinger108eda22018-06-06 18:45:12 -040083
84
Mike Frysinger254f33f2019-12-11 13:54:29 -050085# How many connections we'll use in parallel. We don't want this to be too high
86# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
87# seems to be good enough for users.
88CONNECTION_LIMIT = 10
89
90
Mike Frysinger031ad0b2013-05-14 18:15:34 -040091COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040092
93# Map the internal names to the ones we normally show on the web ui.
94GERRIT_APPROVAL_MAP = {
Alex Klein1699fab2022-09-08 08:46:06 -060095 "COMR": [
96 "CQ",
97 "Commit Queue ",
98 ],
99 "CRVW": [
100 "CR",
101 "Code Review ",
102 ],
103 "SUBM": [
104 "S ",
105 "Submitted ",
106 ],
107 "VRIF": [
108 "V ",
109 "Verified ",
110 ],
111 "LCQ": [
112 "L ",
113 "Legacy ",
114 ],
Mike Frysinger13f23a42013-05-13 17:32:01 -0400115}
116
117# Order is important -- matches the web ui. This also controls the short
118# entries that we summarize in non-verbose mode.
Alex Klein1699fab2022-09-08 08:46:06 -0600119GERRIT_SUMMARY_CATS = (
120 "CR",
121 "CQ",
122 "V",
123)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400124
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400125# Shorter strings for CL status messages.
126GERRIT_SUMMARY_MAP = {
Alex Klein1699fab2022-09-08 08:46:06 -0600127 "ABANDONED": "ABD",
128 "MERGED": "MRG",
129 "NEW": "NEW",
130 "WIP": "WIP",
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400131}
132
Mike Frysinger13f23a42013-05-13 17:32:01 -0400133
Jack Rosenthale3a92672022-06-29 14:54:48 -0600134class OutputFormat(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -0600135 """Type for the requested output format.
Jack Rosenthale3a92672022-06-29 14:54:48 -0600136
Alex Klein1699fab2022-09-08 08:46:06 -0600137 AUTO: Automatically determine the format based on what the user
138 might want. This is PRETTY if attached to a terminal, RAW
139 otherwise.
140 RAW: Output CLs one per line, suitable for mild scripting.
141 JSON: JSON-encoded output, suitable for spicy scripting.
142 MARKDOWN: Suitable for posting in a bug or CL comment.
143 PRETTY: Suitable for viewing in a color terminal.
144 """
145
146 AUTO = 0
147 AUTOMATIC = AUTO
148 RAW = 1
149 JSON = 2
150 MARKDOWN = 3
151 PRETTY = 4
Jack Rosenthale3a92672022-06-29 14:54:48 -0600152
153
Mike Frysinger13f23a42013-05-13 17:32:01 -0400154def red(s):
Alex Klein1699fab2022-09-08 08:46:06 -0600155 return COLOR.Color(terminal.Color.RED, s)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400156
157
158def green(s):
Alex Klein1699fab2022-09-08 08:46:06 -0600159 return COLOR.Color(terminal.Color.GREEN, s)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400160
161
162def blue(s):
Alex Klein1699fab2022-09-08 08:46:06 -0600163 return COLOR.Color(terminal.Color.BLUE, s)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400164
165
Mike Frysinger5d615452023-08-21 10:51:32 -0400166def _run_parallel_tasks(task, jobs: int, *args):
Alex Klein1699fab2022-09-08 08:46:06 -0600167 """Small wrapper around BackgroundTaskRunner to enforce job count."""
Mike Frysinger16474792023-03-01 01:18:00 -0500168
Alex Klein1699fab2022-09-08 08:46:06 -0600169 # When we run in parallel, we can hit the max requests limit.
170 def check_exc(e):
171 if not isinstance(e, gob_util.GOBError):
172 raise e
173 return e.http_status == 429
Mike Frysingera9751c92021-04-30 10:12:37 -0400174
Alex Klein1699fab2022-09-08 08:46:06 -0600175 @retry_util.WithRetry(5, handler=check_exc, sleep=1, backoff_factor=2)
176 def retry(*args):
177 try:
178 task(*args)
179 except gob_util.GOBError as e:
180 if e.http_status != 429:
181 logging.warning("%s: skipping due: %s", args, e)
182 else:
183 raise
Mike Frysingera9751c92021-04-30 10:12:37 -0400184
Mike Frysinger5d615452023-08-21 10:51:32 -0400185 with parallel.BackgroundTaskRunner(retry, processes=jobs) as q:
Alex Klein1699fab2022-09-08 08:46:06 -0600186 for arg in args:
187 q.put([arg])
Mike Frysinger254f33f2019-12-11 13:54:29 -0500188
189
Mike Frysinger13f23a42013-05-13 17:32:01 -0400190def limits(cls):
Alex Klein1699fab2022-09-08 08:46:06 -0600191 """Given a dict of fields, calculate the longest string lengths
Mike Frysinger13f23a42013-05-13 17:32:01 -0400192
Alex Klein1699fab2022-09-08 08:46:06 -0600193 This allows you to easily format the output of many results so that the
194 various cols all line up correctly.
195 """
196 lims = {}
197 for cl in cls:
198 for k in cl.keys():
199 # Use %s rather than str() to avoid codec issues.
200 # We also do this so we can format integers.
201 lims[k] = max(lims.get(k, 0), len("%s" % cl[k]))
202 return lims
Mike Frysinger13f23a42013-05-13 17:32:01 -0400203
204
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400205def process_add_remove_lists(
206 items: List[str], validate: Optional[str] = None
207) -> Tuple[Set[str], Set[str]]:
208 """Split |items| into "add" and "remove" lists.
209
210 Invalid items will cause the program to exit with an error message.
211
212 Args:
Mike Frysingerd74a8bc2023-06-22 09:42:55 -0400213 items: Items that begin with "~" or "-" mean "remove" while others are
214 "add".
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400215 validate: A regular expression to validate each item.
216
217 Returns:
218 A tuple of sets: all the items to add and all the items to remove.
Mike Frysingerd74a8bc2023-06-22 09:42:55 -0400219 NB: The leading "~" & "-" will automatically be stripped.
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400220 """
221 validator = re.compile(validate) if validate else None
222
223 add_list, remove_list, invalid_list = set(), set(), set()
224 for item in items:
225 if not item:
226 invalid_list.add(item)
227 continue
228
229 remove = False
Mike Frysingerd74a8bc2023-06-22 09:42:55 -0400230 if item[0] in ("~", "-"):
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400231 remove = True
232 item = item[1:]
233
234 if validator and not validator.match(item):
235 invalid_list.add(item)
236 elif remove:
237 remove_list.add(item)
238 add_list.discard(item)
239 else:
240 add_list.add(item)
241 remove_list.discard(item)
242
243 if invalid_list:
244 cros_build_lib.Die("Invalid arguments: %s", ", ".join(invalid_list))
245
246 return (add_list, remove_list)
247
248
Mike Frysinger88f27292014-06-17 09:40:45 -0700249# TODO: This func really needs to be merged into the core gerrit logic.
250def GetGerrit(opts, cl=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600251 """Auto pick the right gerrit instance based on the |cl|
Mike Frysinger88f27292014-06-17 09:40:45 -0700252
Alex Klein1699fab2022-09-08 08:46:06 -0600253 Args:
Trent Apted66736d82023-05-25 10:38:28 +1000254 opts: The general options object.
255 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
Mike Frysinger88f27292014-06-17 09:40:45 -0700256
Alex Klein1699fab2022-09-08 08:46:06 -0600257 Returns:
Trent Apted66736d82023-05-25 10:38:28 +1000258 A tuple of a gerrit object and a sanitized CL #.
Alex Klein1699fab2022-09-08 08:46:06 -0600259 """
260 gob = opts.gob
261 if cl is not None:
262 if cl.startswith("*") or cl.startswith("chrome-internal:"):
263 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
264 if cl.startswith("*"):
265 cl = cl[1:]
266 else:
267 cl = cl[16:]
268 elif ":" in cl:
269 gob, cl = cl.split(":", 1)
Mike Frysinger88f27292014-06-17 09:40:45 -0700270
Alex Klein1699fab2022-09-08 08:46:06 -0600271 if not gob in opts.gerrit:
272 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
Mike Frysinger88f27292014-06-17 09:40:45 -0700273
Alex Klein1699fab2022-09-08 08:46:06 -0600274 return (opts.gerrit[gob], cl)
Mike Frysinger88f27292014-06-17 09:40:45 -0700275
276
Mike Frysinger13f23a42013-05-13 17:32:01 -0400277def GetApprovalSummary(_opts, cls):
Alex Klein1699fab2022-09-08 08:46:06 -0600278 """Return a dict of the most important approvals"""
Alex Kleine37b8762023-04-17 12:16:15 -0600279 approvs = {x: "" for x in GERRIT_SUMMARY_CATS}
Alex Klein1699fab2022-09-08 08:46:06 -0600280 for approver in cls.get("currentPatchSet", {}).get("approvals", []):
281 cats = GERRIT_APPROVAL_MAP.get(approver["type"])
282 if not cats:
283 logging.warning(
284 "unknown gerrit approval type: %s", approver["type"]
285 )
286 continue
287 cat = cats[0].strip()
288 val = int(approver["value"])
289 if not cat in approvs:
290 # Ignore the extended categories in the summary view.
291 continue
292 elif approvs[cat] == "":
293 approvs[cat] = val
294 elif val < 0:
295 approvs[cat] = min(approvs[cat], val)
296 else:
297 approvs[cat] = max(approvs[cat], val)
298 return approvs
Mike Frysinger13f23a42013-05-13 17:32:01 -0400299
300
Mike Frysingera1b4b272017-04-05 16:11:00 -0400301def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600302 """Pretty print a single result"""
303 if lims is None:
304 lims = {"url": 0, "project": 0}
Mike Frysinger13f23a42013-05-13 17:32:01 -0400305
Alex Klein1699fab2022-09-08 08:46:06 -0600306 status = ""
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400307
Alex Klein1699fab2022-09-08 08:46:06 -0600308 if opts.verbose:
309 status += "%s " % (cl["status"],)
310 else:
311 status += "%s " % (GERRIT_SUMMARY_MAP.get(cl["status"], cl["status"]),)
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400312
Alex Klein1699fab2022-09-08 08:46:06 -0600313 if show_approvals and not opts.verbose:
314 approvs = GetApprovalSummary(opts, cl)
315 for cat in GERRIT_SUMMARY_CATS:
316 if approvs[cat] in ("", 0):
317 functor = lambda x: x
318 elif approvs[cat] < 0:
319 functor = red
320 else:
321 functor = green
322 status += functor("%s:%2s " % (cat, approvs[cat]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400323
Alex Klein1699fab2022-09-08 08:46:06 -0600324 if opts.format is OutputFormat.MARKDOWN:
325 print("* %s - %s" % (uri_lib.ShortenUri(cl["url"]), cl["subject"]))
326 else:
327 print(
328 "%s %s%-*s %s"
329 % (
330 blue("%-*s" % (lims["url"], cl["url"])),
331 status,
332 lims["project"],
333 cl["project"],
334 cl["subject"],
335 )
336 )
Mike Frysinger13f23a42013-05-13 17:32:01 -0400337
Alex Klein1699fab2022-09-08 08:46:06 -0600338 if show_approvals and opts.verbose:
339 for approver in cl["currentPatchSet"].get("approvals", []):
340 functor = red if int(approver["value"]) < 0 else green
341 n = functor("%2s" % approver["value"])
342 t = GERRIT_APPROVAL_MAP.get(
343 approver["type"], [approver["type"], approver["type"]]
344 )[1]
345 print(" %s %s %s" % (n, t, approver["by"]["email"]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400346
347
Mike Frysingera1b4b272017-04-05 16:11:00 -0400348def PrintCls(opts, cls, lims=None, show_approvals=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600349 """Print all results based on the requested format."""
350 if opts.format is OutputFormat.RAW:
351 site_params = config_lib.GetSiteParams()
352 pfx = ""
353 # Special case internal Chrome GoB as that is what most devs use.
354 # They can always redirect the list elsewhere via the -g option.
355 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
356 pfx = site_params.INTERNAL_CHANGE_PREFIX
357 for cl in cls:
358 print("%s%s" % (pfx, cl["number"]))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400359
Alex Klein1699fab2022-09-08 08:46:06 -0600360 elif opts.format is OutputFormat.JSON:
361 json.dump(cls, sys.stdout)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400362
Alex Klein1699fab2022-09-08 08:46:06 -0600363 else:
364 if lims is None:
365 lims = limits(cls)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400366
Alex Klein1699fab2022-09-08 08:46:06 -0600367 for cl in cls:
368 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400369
370
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400371def _Query(opts, query, raw=True, helper=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600372 """Queries Gerrit with a query string built from the commandline options"""
373 if opts.branch is not None:
374 query += " branch:%s" % opts.branch
375 if opts.project is not None:
376 query += " project: %s" % opts.project
377 if opts.topic is not None:
378 query += " topic: %s" % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800379
Alex Klein1699fab2022-09-08 08:46:06 -0600380 if helper is None:
381 helper, _ = GetGerrit(opts)
382 return helper.Query(query, raw=raw, bypass_cache=False)
Paul Hobbs89765232015-06-24 14:07:49 -0700383
384
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400385def FilteredQuery(opts, query, helper=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600386 """Query gerrit and filter/clean up the results"""
387 ret = []
Paul Hobbs89765232015-06-24 14:07:49 -0700388
Alex Klein1699fab2022-09-08 08:46:06 -0600389 logging.debug("Running query: %s", query)
390 for cl in _Query(opts, query, raw=True, helper=helper):
391 # Gerrit likes to return a stats record too.
392 if not "project" in cl:
393 continue
Mike Frysinger13f23a42013-05-13 17:32:01 -0400394
Alex Klein1699fab2022-09-08 08:46:06 -0600395 # Strip off common leading names since the result is still
396 # unique over the whole tree.
397 if not opts.verbose:
398 for pfx in (
399 "aosp",
400 "chromeos",
401 "chromiumos",
402 "external",
403 "overlays",
404 "platform",
405 "third_party",
406 ):
407 if cl["project"].startswith("%s/" % pfx):
408 cl["project"] = cl["project"][len(pfx) + 1 :]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400409
Alex Klein1699fab2022-09-08 08:46:06 -0600410 cl["url"] = uri_lib.ShortenUri(cl["url"])
Mike Frysinger479f1192017-09-14 22:36:30 -0400411
Alex Klein1699fab2022-09-08 08:46:06 -0600412 ret.append(cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400413
Alex Klein1699fab2022-09-08 08:46:06 -0600414 if opts.sort == "unsorted":
415 return ret
416 if opts.sort == "number":
417 key = lambda x: int(x[opts.sort])
418 else:
419 key = lambda x: x[opts.sort]
420 return sorted(ret, key=key)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400421
422
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500423class _ActionSearchQuery(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600424 """Base class for actions that perform searches."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500425
Alex Klein1699fab2022-09-08 08:46:06 -0600426 USE_PAGER = True
Jack Rosenthal95aac172022-06-30 15:35:07 -0600427
Alex Klein1699fab2022-09-08 08:46:06 -0600428 @staticmethod
429 def init_subparser(parser):
430 """Add arguments to this action's subparser."""
431 parser.add_argument(
432 "--sort",
433 default="number",
Trent Apted66736d82023-05-25 10:38:28 +1000434 help='Key to sort on (number, project); use "unsorted" to disable',
Alex Klein1699fab2022-09-08 08:46:06 -0600435 )
436 parser.add_argument(
437 "-b", "--branch", help="Limit output to the specific branch"
438 )
439 parser.add_argument(
440 "-p", "--project", help="Limit output to the specific project"
441 )
442 parser.add_argument(
443 "-t", "--topic", help="Limit output to the specific topic"
444 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500445
446
447class ActionTodo(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600448 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500449
Alex Klein1699fab2022-09-08 08:46:06 -0600450 COMMAND = "todo"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500451
Alex Klein1699fab2022-09-08 08:46:06 -0600452 @staticmethod
453 def __call__(opts):
454 """Implement the action."""
455 cls = FilteredQuery(opts, "attention:self")
456 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400457
458
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500459class ActionSearch(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600460 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500461
Alex Klein1699fab2022-09-08 08:46:06 -0600462 COMMAND = "search"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500463
Alex Klein1699fab2022-09-08 08:46:06 -0600464 @staticmethod
465 def init_subparser(parser):
466 """Add arguments to this action's subparser."""
467 _ActionSearchQuery.init_subparser(parser)
468 parser.add_argument("query", help="The search query")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500469
Alex Klein1699fab2022-09-08 08:46:06 -0600470 @staticmethod
471 def __call__(opts):
472 """Implement the action."""
473 cls = FilteredQuery(opts, opts.query)
474 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400475
476
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500477class ActionMine(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600478 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500479
Alex Klein1699fab2022-09-08 08:46:06 -0600480 COMMAND = "mine"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500481
Alex Klein1699fab2022-09-08 08:46:06 -0600482 @staticmethod
483 def init_subparser(parser):
484 """Add arguments to this action's subparser."""
485 _ActionSearchQuery.init_subparser(parser)
486 parser.add_argument(
487 "--draft",
488 default=False,
489 action="store_true",
490 help="Show draft changes",
491 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500492
Alex Klein1699fab2022-09-08 08:46:06 -0600493 @staticmethod
494 def __call__(opts):
495 """Implement the action."""
496 if opts.draft:
497 rule = "is:draft"
498 else:
499 rule = "status:new"
500 cls = FilteredQuery(opts, "owner:self %s" % (rule,))
501 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700502
503
Paul Hobbs89765232015-06-24 14:07:49 -0700504def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
Alex Klein1699fab2022-09-08 08:46:06 -0600505 """Runs breadth first search starting from the nodes in |to_visit|
Paul Hobbs89765232015-06-24 14:07:49 -0700506
Alex Klein1699fab2022-09-08 08:46:06 -0600507 Args:
Trent Apted66736d82023-05-25 10:38:28 +1000508 to_visit: the starting nodes
509 children: a function which takes a node and returns the adjacent nodes
510 visited_key: a function for deduplicating node visits. Defaults to the
511 identity function (lambda x: x)
Paul Hobbs89765232015-06-24 14:07:49 -0700512
Alex Klein1699fab2022-09-08 08:46:06 -0600513 Returns:
Trent Apted66736d82023-05-25 10:38:28 +1000514 A list of nodes which are reachable from any node in |to_visit| by
515 calling
516 |children| any number of times.
Alex Klein1699fab2022-09-08 08:46:06 -0600517 """
518 to_visit = list(to_visit)
519 seen = set(visited_key(x) for x in to_visit)
520 for node in to_visit:
521 for child in children(node):
522 key = visited_key(child)
523 if key not in seen:
524 seen.add(key)
525 to_visit.append(child)
526 return to_visit
Paul Hobbs89765232015-06-24 14:07:49 -0700527
528
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500529class ActionDeps(_ActionSearchQuery):
Alex Klein4507b172023-01-13 11:39:51 -0700530 """List CLs matching a query, and transitive dependencies of those CLs."""
Paul Hobbs89765232015-06-24 14:07:49 -0700531
Alex Klein1699fab2022-09-08 08:46:06 -0600532 COMMAND = "deps"
Paul Hobbs89765232015-06-24 14:07:49 -0700533
Alex Klein1699fab2022-09-08 08:46:06 -0600534 @staticmethod
535 def init_subparser(parser):
536 """Add arguments to this action's subparser."""
537 _ActionSearchQuery.init_subparser(parser)
538 parser.add_argument("query", help="The search query")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500539
Alex Klein1699fab2022-09-08 08:46:06 -0600540 def __call__(self, opts):
541 """Implement the action."""
542 cls = _Query(opts, opts.query, raw=False)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500543
Alex Klein1699fab2022-09-08 08:46:06 -0600544 @memoize.Memoize
545 def _QueryChange(cl, helper=None):
546 return _Query(opts, cl, raw=False, helper=helper)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500547
Alex Klein1699fab2022-09-08 08:46:06 -0600548 transitives = _BreadthFirstSearch(
549 cls,
550 functools.partial(self._Children, opts, _QueryChange),
551 visited_key=lambda cl: cl.PatchLink(),
552 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500553
Alex Klein1699fab2022-09-08 08:46:06 -0600554 # This is a hack to avoid losing GoB host for each CL. The PrintCls
555 # function assumes the GoB host specified by the user is the only one
556 # that is ever used, but the deps command walks across hosts.
557 if opts.format is OutputFormat.RAW:
558 print("\n".join(x.PatchLink() for x in transitives))
559 else:
560 transitives_raw = [cl.patch_dict for cl in transitives]
561 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500562
Alex Klein1699fab2022-09-08 08:46:06 -0600563 @staticmethod
564 def _ProcessDeps(opts, querier, cl, deps, required):
565 """Yields matching dependencies for a patch"""
566 # We need to query the change to guarantee that we have a .gerrit_number
567 for dep in deps:
568 if not dep.remote in opts.gerrit:
569 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
570 remote=dep.remote, print_cmd=opts.debug
571 )
572 helper = opts.gerrit[dep.remote]
Mike Frysingerb3300c42017-07-20 01:41:17 -0400573
Alex Klein1699fab2022-09-08 08:46:06 -0600574 # TODO(phobbs) this should maybe catch network errors.
575 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400576
Alex Klein4507b172023-01-13 11:39:51 -0700577 # Handle empty results. If we found a commit that was pushed
578 # directly (e.g. a bot commit), then gerrit won't know about it.
Alex Klein1699fab2022-09-08 08:46:06 -0600579 if not changes:
580 if required:
581 logging.error(
582 "CL %s depends on %s which cannot be found",
583 cl,
584 dep.ToGerritQueryText(),
585 )
586 continue
Mike Frysinger5726da92017-09-20 22:14:25 -0400587
Alex Klein4507b172023-01-13 11:39:51 -0700588 # Our query might have matched more than one result. This can come
589 # up when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id
590 # shows up across multiple repos/branches. We blindly check all of
591 # them in the hopes that all open ones are what the user wants, but
592 # then again the CQ-DEPEND syntax itself is unable to differentiate.
593 # *shrug*
Alex Klein1699fab2022-09-08 08:46:06 -0600594 if len(changes) > 1:
595 logging.warning(
596 "CL %s has an ambiguous CQ dependency %s",
597 cl,
598 dep.ToGerritQueryText(),
599 )
600 for change in changes:
601 if change.status == "NEW":
602 yield change
Mike Frysinger5726da92017-09-20 22:14:25 -0400603
Alex Klein1699fab2022-09-08 08:46:06 -0600604 @classmethod
605 def _Children(cls, opts, querier, cl):
606 """Yields the Gerrit dependencies of a patch"""
607 for change in cls._ProcessDeps(
608 opts, querier, cl, cl.GerritDependencies(), False
609 ):
610 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700611
Paul Hobbs89765232015-06-24 14:07:49 -0700612
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500613class ActionInspect(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600614 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500615
Alex Klein1699fab2022-09-08 08:46:06 -0600616 COMMAND = "inspect"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500617
Alex Klein1699fab2022-09-08 08:46:06 -0600618 @staticmethod
619 def init_subparser(parser):
620 """Add arguments to this action's subparser."""
621 _ActionSearchQuery.init_subparser(parser)
622 parser.add_argument(
623 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
624 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500625
Alex Klein1699fab2022-09-08 08:46:06 -0600626 @staticmethod
627 def __call__(opts):
628 """Implement the action."""
629 cls = []
630 for arg in opts.cls:
631 helper, cl = GetGerrit(opts, arg)
632 change = FilteredQuery(opts, "change:%s" % cl, helper=helper)
633 if change:
634 cls.extend(change)
635 else:
636 logging.warning("no results found for CL %s", arg)
637 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400638
639
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500640class _ActionLabeler(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600641 """Base helper for setting labels."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500642
Alex Klein1699fab2022-09-08 08:46:06 -0600643 LABEL = None
644 VALUES = None
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500645
Alex Klein1699fab2022-09-08 08:46:06 -0600646 @classmethod
647 def init_subparser(cls, parser):
648 """Add arguments to this action's subparser."""
649 parser.add_argument(
650 "-m",
651 "--msg",
652 "--message",
653 metavar="MESSAGE",
654 help="Optional message to include",
655 )
656 parser.add_argument(
657 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
658 )
659 parser.add_argument(
660 "value",
661 nargs=1,
662 metavar="value",
663 choices=cls.VALUES,
664 help="The label value; one of [%(choices)s]",
665 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500666
Alex Klein1699fab2022-09-08 08:46:06 -0600667 @classmethod
668 def __call__(cls, opts):
669 """Implement the action."""
Mike Frysinger16474792023-03-01 01:18:00 -0500670
Alex Klein1699fab2022-09-08 08:46:06 -0600671 # Convert user-friendly command line option into a gerrit parameter.
672 def task(arg):
673 helper, cl = GetGerrit(opts, arg)
674 helper.SetReview(
675 cl,
676 labels={cls.LABEL: opts.value[0]},
677 msg=opts.msg,
678 dryrun=opts.dryrun,
679 notify=opts.notify,
680 )
681
Mike Frysinger5d615452023-08-21 10:51:32 -0400682 _run_parallel_tasks(task, opts.jobs, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500683
684
685class ActionLabelAutoSubmit(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600686 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500687
Alex Klein1699fab2022-09-08 08:46:06 -0600688 COMMAND = "label-as"
689 LABEL = "Auto-Submit"
690 VALUES = ("0", "1")
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600691
692
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500693class ActionLabelCodeReview(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600694 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500695
Alex Klein1699fab2022-09-08 08:46:06 -0600696 COMMAND = "label-cr"
697 LABEL = "Code-Review"
698 VALUES = ("-2", "-1", "0", "1", "2")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400699
700
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500701class ActionLabelVerified(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600702 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500703
Alex Klein1699fab2022-09-08 08:46:06 -0600704 COMMAND = "label-v"
705 LABEL = "Verified"
706 VALUES = ("-1", "0", "1")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400707
708
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500709class ActionLabelCommitQueue(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600710 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500711
Alex Klein1699fab2022-09-08 08:46:06 -0600712 COMMAND = "label-cq"
713 LABEL = "Commit-Queue"
714 VALUES = ("0", "1", "2")
715
Mike Frysinger15b23e42014-12-05 17:00:05 -0500716
C Shapiro3f1f8242021-08-02 15:28:29 -0500717class ActionLabelOwnersOverride(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600718 """Change the Owners-Override label (1=Override)"""
C Shapiro3f1f8242021-08-02 15:28:29 -0500719
Alex Klein1699fab2022-09-08 08:46:06 -0600720 COMMAND = "label-oo"
721 LABEL = "Owners-Override"
722 VALUES = ("0", "1")
C Shapiro3f1f8242021-08-02 15:28:29 -0500723
Mike Frysinger15b23e42014-12-05 17:00:05 -0500724
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500725class _ActionSimpleParallelCLs(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600726 """Base helper for actions that only accept CLs."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500727
Alex Klein1699fab2022-09-08 08:46:06 -0600728 @staticmethod
729 def init_subparser(parser):
730 """Add arguments to this action's subparser."""
731 parser.add_argument(
732 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
733 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500734
Alex Klein1699fab2022-09-08 08:46:06 -0600735 def __call__(self, opts):
736 """Implement the action."""
737
738 def task(arg):
739 helper, cl = GetGerrit(opts, arg)
740 self._process_one(helper, cl, opts)
741
Mike Frysinger5d615452023-08-21 10:51:32 -0400742 _run_parallel_tasks(task, opts.jobs, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500743
744
745class ActionSubmit(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600746 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500747
Alex Klein1699fab2022-09-08 08:46:06 -0600748 COMMAND = "submit"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500749
Alex Klein1699fab2022-09-08 08:46:06 -0600750 @staticmethod
751 def _process_one(helper, cl, opts):
752 """Use |helper| to process the single |cl|."""
753 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400754
755
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500756class ActionAbandon(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600757 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500758
Alex Klein1699fab2022-09-08 08:46:06 -0600759 COMMAND = "abandon"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500760
Alex Klein1699fab2022-09-08 08:46:06 -0600761 @staticmethod
762 def init_subparser(parser):
763 """Add arguments to this action's subparser."""
764 parser.add_argument(
765 "-m",
766 "--msg",
767 "--message",
768 metavar="MESSAGE",
769 help="Include a message",
770 )
771 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger3af378b2021-03-12 01:34:04 -0500772
Alex Klein1699fab2022-09-08 08:46:06 -0600773 @staticmethod
774 def _process_one(helper, cl, opts):
775 """Use |helper| to process the single |cl|."""
776 helper.AbandonChange(
777 cl, msg=opts.msg, dryrun=opts.dryrun, notify=opts.notify
778 )
Mike Frysinger13f23a42013-05-13 17:32:01 -0400779
780
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500781class ActionRestore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600782 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500783
Alex Klein1699fab2022-09-08 08:46:06 -0600784 COMMAND = "restore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500785
Alex Klein1699fab2022-09-08 08:46:06 -0600786 @staticmethod
787 def _process_one(helper, cl, opts):
788 """Use |helper| to process the single |cl|."""
789 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400790
791
Tomasz Figa54d70992021-01-20 13:48:59 +0900792class ActionWorkInProgress(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600793 """Mark CLs as work in progress"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900794
Alex Klein1699fab2022-09-08 08:46:06 -0600795 COMMAND = "wip"
Tomasz Figa54d70992021-01-20 13:48:59 +0900796
Alex Klein1699fab2022-09-08 08:46:06 -0600797 @staticmethod
798 def _process_one(helper, cl, opts):
799 """Use |helper| to process the single |cl|."""
800 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900801
802
803class ActionReadyForReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600804 """Mark CLs as ready for review"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900805
Alex Klein1699fab2022-09-08 08:46:06 -0600806 COMMAND = "ready"
Tomasz Figa54d70992021-01-20 13:48:59 +0900807
Alex Klein1699fab2022-09-08 08:46:06 -0600808 @staticmethod
809 def _process_one(helper, cl, opts):
810 """Use |helper| to process the single |cl|."""
811 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900812
813
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500814class ActionReviewers(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600815 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700816
Alex Klein1699fab2022-09-08 08:46:06 -0600817 COMMAND = "reviewers"
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700818
Alex Klein1699fab2022-09-08 08:46:06 -0600819 @staticmethod
820 def init_subparser(parser):
821 """Add arguments to this action's subparser."""
822 parser.add_argument("cl", metavar="CL", help="The CL to update")
823 parser.add_argument(
824 "reviewers", nargs="+", help="The reviewers to add/remove"
825 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700826
Alex Klein1699fab2022-09-08 08:46:06 -0600827 @staticmethod
828 def __call__(opts):
829 """Implement the action."""
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400830 add_list, remove_list = process_add_remove_lists(
831 opts.reviewers, f"^{constants.EMAIL_REGEX}$"
832 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500833
Alex Klein1699fab2022-09-08 08:46:06 -0600834 if add_list or remove_list:
835 helper, cl = GetGerrit(opts, opts.cl)
836 helper.SetReviewers(
837 cl,
838 add=add_list,
839 remove=remove_list,
840 dryrun=opts.dryrun,
841 notify=opts.notify,
842 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700843
844
Brian Norrisd25af082021-10-29 11:25:31 -0700845class ActionAttentionSet(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600846 """Add/remove emails from the attention set (prepend with '~' to remove)"""
Brian Norrisd25af082021-10-29 11:25:31 -0700847
Alex Klein1699fab2022-09-08 08:46:06 -0600848 COMMAND = "attention"
Brian Norrisd25af082021-10-29 11:25:31 -0700849
Alex Klein1699fab2022-09-08 08:46:06 -0600850 @staticmethod
851 def init_subparser(parser):
852 """Add arguments to this action's subparser."""
853 parser.add_argument(
854 "-m",
855 "--msg",
856 "--message",
857 metavar="MESSAGE",
858 help="Optional message to include",
859 default="gerrit CLI",
860 )
861 parser.add_argument("cl", metavar="CL", help="The CL to update")
862 parser.add_argument(
863 "users",
864 nargs="+",
865 help="The users to add/remove from attention set",
866 )
Brian Norrisd25af082021-10-29 11:25:31 -0700867
Alex Klein1699fab2022-09-08 08:46:06 -0600868 @staticmethod
869 def __call__(opts):
870 """Implement the action."""
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400871 add_list, remove_list = process_add_remove_lists(
872 opts.users, f"^{constants.EMAIL_REGEX}$"
873 )
Brian Norrisd25af082021-10-29 11:25:31 -0700874
Alex Klein1699fab2022-09-08 08:46:06 -0600875 if add_list or remove_list:
876 helper, cl = GetGerrit(opts, opts.cl)
877 helper.SetAttentionSet(
878 cl,
879 add=add_list,
880 remove=remove_list,
881 dryrun=opts.dryrun,
882 notify=opts.notify,
883 message=opts.msg,
884 )
Brian Norrisd25af082021-10-29 11:25:31 -0700885
886
Mike Frysinger62178ae2020-03-20 01:37:43 -0400887class ActionMessage(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600888 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500889
Alex Klein1699fab2022-09-08 08:46:06 -0600890 COMMAND = "message"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500891
Alex Klein1699fab2022-09-08 08:46:06 -0600892 @staticmethod
893 def init_subparser(parser):
894 """Add arguments to this action's subparser."""
895 _ActionSimpleParallelCLs.init_subparser(parser)
896 parser.add_argument("message", help="The message to post")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500897
Alex Klein1699fab2022-09-08 08:46:06 -0600898 @staticmethod
899 def _process_one(helper, cl, opts):
900 """Use |helper| to process the single |cl|."""
901 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530902
903
Mike Frysinger62178ae2020-03-20 01:37:43 -0400904class ActionTopic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600905 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500906
Alex Klein1699fab2022-09-08 08:46:06 -0600907 COMMAND = "topic"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500908
Alex Klein1699fab2022-09-08 08:46:06 -0600909 @staticmethod
910 def init_subparser(parser):
911 """Add arguments to this action's subparser."""
912 _ActionSimpleParallelCLs.init_subparser(parser)
913 parser.add_argument("topic", help="The topic to set")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500914
Alex Klein1699fab2022-09-08 08:46:06 -0600915 @staticmethod
916 def _process_one(helper, cl, opts):
917 """Use |helper| to process the single |cl|."""
918 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800919
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800920
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500921class ActionPrivate(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600922 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700923
Alex Klein1699fab2022-09-08 08:46:06 -0600924 COMMAND = "private"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500925
Alex Klein1699fab2022-09-08 08:46:06 -0600926 @staticmethod
927 def _process_one(helper, cl, opts):
928 """Use |helper| to process the single |cl|."""
929 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700930
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800931
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500932class ActionPublic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600933 """Mark CLs public"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500934
Alex Klein1699fab2022-09-08 08:46:06 -0600935 COMMAND = "public"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500936
Alex Klein1699fab2022-09-08 08:46:06 -0600937 @staticmethod
938 def _process_one(helper, cl, opts):
939 """Use |helper| to process the single |cl|."""
940 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500941
942
943class ActionSethashtags(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600944 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500945
Alex Klein1699fab2022-09-08 08:46:06 -0600946 COMMAND = "hashtags"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500947
Alex Klein1699fab2022-09-08 08:46:06 -0600948 @staticmethod
949 def init_subparser(parser):
950 """Add arguments to this action's subparser."""
951 parser.add_argument("cl", metavar="CL", help="The CL to update")
952 parser.add_argument(
953 "hashtags", nargs="+", help="The hashtags to add/remove"
954 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500955
Alex Klein1699fab2022-09-08 08:46:06 -0600956 @staticmethod
957 def __call__(opts):
958 """Implement the action."""
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400959 add, remove = process_add_remove_lists(opts.hashtags)
Alex Klein1699fab2022-09-08 08:46:06 -0600960 helper, cl = GetGerrit(opts, opts.cl)
Cheng Yuehb0ed9462023-08-14 14:06:33 +0800961 helper.SetHashtags(cl, list(add), list(remove), dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800962
963
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400964class ActionDelete(_ActionSimpleParallelCLs):
965 """Delete CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500966
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400967 COMMAND = "delete"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500968
Alex Klein1699fab2022-09-08 08:46:06 -0600969 @staticmethod
970 def _process_one(helper, cl, opts):
971 """Use |helper| to process the single |cl|."""
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400972 helper.Delete(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800973
974
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500975class ActionReviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600976 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500977
Alex Klein1699fab2022-09-08 08:46:06 -0600978 COMMAND = "reviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500979
Alex Klein1699fab2022-09-08 08:46:06 -0600980 @staticmethod
981 def _process_one(helper, cl, opts):
982 """Use |helper| to process the single |cl|."""
983 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500984
985
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500986class ActionUnreviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600987 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500988
Alex Klein1699fab2022-09-08 08:46:06 -0600989 COMMAND = "unreviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500990
Alex Klein1699fab2022-09-08 08:46:06 -0600991 @staticmethod
992 def _process_one(helper, cl, opts):
993 """Use |helper| to process the single |cl|."""
994 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500995
996
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500997class ActionIgnore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600998 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500999
Alex Klein1699fab2022-09-08 08:46:06 -06001000 COMMAND = "ignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001001
Alex Klein1699fab2022-09-08 08:46:06 -06001002 @staticmethod
1003 def _process_one(helper, cl, opts):
1004 """Use |helper| to process the single |cl|."""
1005 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -05001006
1007
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001008class ActionUnignore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001009 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001010
Alex Klein1699fab2022-09-08 08:46:06 -06001011 COMMAND = "unignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001012
Alex Klein1699fab2022-09-08 08:46:06 -06001013 @staticmethod
1014 def _process_one(helper, cl, opts):
1015 """Use |helper| to process the single |cl|."""
1016 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -05001017
1018
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001019class ActionCherryPick(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001020 """Cherry-pick CLs to branches."""
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001021
Alex Klein1699fab2022-09-08 08:46:06 -06001022 COMMAND = "cherry-pick"
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001023
Alex Klein1699fab2022-09-08 08:46:06 -06001024 @staticmethod
1025 def init_subparser(parser):
1026 """Add arguments to this action's subparser."""
1027 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
1028 parser.add_argument(
1029 "--rev",
1030 "--revision",
1031 default="current",
1032 help="A specific revision or patchset",
1033 )
1034 parser.add_argument(
1035 "-m",
1036 "--msg",
1037 "--message",
1038 metavar="MESSAGE",
1039 help="Include a message",
1040 )
1041 parser.add_argument(
1042 "--branches",
1043 "--branch",
1044 "--br",
1045 action="split_extend",
1046 default=[],
1047 required=True,
1048 help="The destination branches",
1049 )
1050 parser.add_argument(
Mike Frysingera8adc4e2023-08-21 13:13:15 -04001051 "--allow-conflicts",
1052 action="store_true",
1053 help="Cherry-pick the CL with conflicts.",
1054 )
1055 parser.add_argument(
Alex Klein1699fab2022-09-08 08:46:06 -06001056 "cls", nargs="+", metavar="CL", help="The CLs to cherry-pick"
1057 )
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001058
Alex Klein1699fab2022-09-08 08:46:06 -06001059 @staticmethod
1060 def __call__(opts):
1061 """Implement the action."""
Mike Frysinger16474792023-03-01 01:18:00 -05001062
Alex Klein1699fab2022-09-08 08:46:06 -06001063 # Process branches in parallel, but CLs in serial in case of CL stacks.
1064 def task(branch):
1065 for arg in opts.cls:
1066 helper, cl = GetGerrit(opts, arg)
1067 ret = helper.CherryPick(
1068 cl,
1069 branch,
1070 rev=opts.rev,
1071 msg=opts.msg,
Mike Frysingera8adc4e2023-08-21 13:13:15 -04001072 allow_conflicts=opts.allow_conflicts,
Alex Klein1699fab2022-09-08 08:46:06 -06001073 dryrun=opts.dryrun,
1074 notify=opts.notify,
1075 )
1076 logging.debug("Response: %s", ret)
1077 if opts.format is OutputFormat.RAW:
1078 print(ret["_number"])
1079 else:
1080 uri = f'https://{helper.host}/c/{ret["_number"]}'
1081 print(uri_lib.ShortenUri(uri))
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001082
Mike Frysinger5d615452023-08-21 10:51:32 -04001083 _run_parallel_tasks(task, opts.jobs, *opts.branches)
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001084
1085
Mike Frysinger8037f752020-02-29 20:47:09 -05001086class ActionReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001087 """Review CLs with multiple settings
Mike Frysinger8037f752020-02-29 20:47:09 -05001088
Mike Frysingerd74a8bc2023-06-22 09:42:55 -04001089 The reviewers & cc options can remove people by prepending '~' or '-'.
1090 Note: If you want to move someone (reviewer->CC or CC->reviewer), you don't
1091 have to remove them first, you only need to specify the final state.
Mike Frysingere5a69832023-06-22 09:34:57 -04001092
Alex Klein4507b172023-01-13 11:39:51 -07001093 The label option supports extended/multiple syntax for easy use. The --label
1094 option may be specified multiple times (as settings are merges), and
1095 multiple labels are allowed in a single argument. Each label has the form:
Alex Klein1699fab2022-09-08 08:46:06 -06001096 <long or short name><=+-><value>
Mike Frysinger8037f752020-02-29 20:47:09 -05001097
Alex Klein1699fab2022-09-08 08:46:06 -06001098 Common arguments:
1099 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
1100 'V+1 CQ+2'
1101 'AS=1 V=1'
1102 """
Mike Frysinger8037f752020-02-29 20:47:09 -05001103
Alex Klein1699fab2022-09-08 08:46:06 -06001104 COMMAND = "review"
Mike Frysinger8037f752020-02-29 20:47:09 -05001105
Alex Klein1699fab2022-09-08 08:46:06 -06001106 class _SetLabel(argparse.Action):
1107 """Argparse action for setting labels."""
Mike Frysinger8037f752020-02-29 20:47:09 -05001108
Alex Klein1699fab2022-09-08 08:46:06 -06001109 LABEL_MAP = {
1110 "AS": "Auto-Submit",
1111 "CQ": "Commit-Queue",
1112 "CR": "Code-Review",
1113 "V": "Verified",
1114 }
Mike Frysinger8037f752020-02-29 20:47:09 -05001115
Alex Klein1699fab2022-09-08 08:46:06 -06001116 def __call__(self, parser, namespace, values, option_string=None):
1117 labels = getattr(namespace, self.dest)
1118 for request in values.split():
1119 if "=" in request:
1120 # Handle Verified=1 form.
1121 short, value = request.split("=", 1)
1122 elif "+" in request:
1123 # Handle Verified+1 form.
1124 short, value = request.split("+", 1)
1125 elif "-" in request:
1126 # Handle Verified-1 form.
1127 short, value = request.split("-", 1)
1128 value = "-%s" % (value,)
1129 else:
1130 parser.error(
1131 'Invalid label setting "%s". Must be Commit-Queue=1 or '
1132 "CQ+1 or CR-1." % (request,)
1133 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001134
Alex Klein1699fab2022-09-08 08:46:06 -06001135 # Convert possible short label names like "V" to "Verified".
1136 label = self.LABEL_MAP.get(short)
1137 if not label:
1138 label = short
Mike Frysinger8037f752020-02-29 20:47:09 -05001139
Alex Klein1699fab2022-09-08 08:46:06 -06001140 # We allow existing label requests to be overridden.
1141 labels[label] = value
Mike Frysinger8037f752020-02-29 20:47:09 -05001142
Alex Klein1699fab2022-09-08 08:46:06 -06001143 @classmethod
1144 def init_subparser(cls, parser):
1145 """Add arguments to this action's subparser."""
1146 parser.add_argument(
1147 "-m",
1148 "--msg",
1149 "--message",
1150 metavar="MESSAGE",
1151 help="Include a message",
1152 )
1153 parser.add_argument(
1154 "-l",
1155 "--label",
1156 dest="labels",
1157 action=cls._SetLabel,
1158 default={},
1159 help="Set a label with a value",
1160 )
1161 parser.add_argument(
1162 "--ready",
1163 default=None,
1164 action="store_true",
1165 help="Set CL status to ready-for-review",
1166 )
1167 parser.add_argument(
1168 "--wip",
1169 default=None,
1170 action="store_true",
1171 help="Set CL status to WIP",
1172 )
1173 parser.add_argument(
1174 "--reviewers",
1175 "--re",
1176 action="append",
1177 default=[],
Mike Frysingere5a69832023-06-22 09:34:57 -04001178 help="Reviewers to add/remove",
Alex Klein1699fab2022-09-08 08:46:06 -06001179 )
1180 parser.add_argument(
Mike Frysingere5a69832023-06-22 09:34:57 -04001181 "--cc",
1182 action="append",
1183 default=[],
1184 help="People to add/remove in CC",
Alex Klein1699fab2022-09-08 08:46:06 -06001185 )
1186 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger8037f752020-02-29 20:47:09 -05001187
Alex Klein1699fab2022-09-08 08:46:06 -06001188 @staticmethod
1189 def _process_one(helper, cl, opts):
1190 """Use |helper| to process the single |cl|."""
Mike Frysingere5a69832023-06-22 09:34:57 -04001191 add_reviewers, remove_reviewers = process_add_remove_lists(
1192 opts.reviewers, f"^{constants.EMAIL_REGEX}$"
1193 )
1194 add_cc, remove_cc = process_add_remove_lists(
1195 opts.cc, f"^{constants.EMAIL_REGEX}$"
1196 )
1197
1198 # Gerrit allows people to only be in one state: CC or Reviewer. If a
1199 # person is in CC and you want to move them to reviewer, you can't
1200 # remove them from CC and add to reviewer, you have to change their
1201 # state. Help users who do `--cc ~u@c --re u@c` by filtering out all
1202 # the remove requests if there is an add request too. This doesn't
1203 # quite respect all the possible CLI option orders, but it's probably
1204 # good enough for now in practice. For example, mixing of CC & reviewer
1205 # and adds & removes gets complicated.
1206 for add in add_cc:
1207 if add in remove_reviewers:
1208 remove_reviewers.remove(add)
1209 for add in add_reviewers:
1210 if add in remove_cc:
1211 remove_cc.remove(add)
1212
Alex Klein1699fab2022-09-08 08:46:06 -06001213 helper.SetReview(
1214 cl,
1215 msg=opts.msg,
1216 labels=opts.labels,
1217 dryrun=opts.dryrun,
1218 notify=opts.notify,
Mike Frysingere5a69832023-06-22 09:34:57 -04001219 reviewers=add_reviewers,
1220 cc=add_cc,
1221 remove_reviewers=remove_reviewers | remove_cc,
Alex Klein1699fab2022-09-08 08:46:06 -06001222 ready=opts.ready,
1223 wip=opts.wip,
1224 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001225
1226
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001227class ActionAccount(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001228 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001229
Alex Klein1699fab2022-09-08 08:46:06 -06001230 COMMAND = "account"
1231 USE_PAGER = True
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001232
Alex Klein1699fab2022-09-08 08:46:06 -06001233 @staticmethod
1234 def init_subparser(parser):
1235 """Add arguments to this action's subparser."""
1236 parser.add_argument(
1237 "accounts",
1238 nargs="*",
1239 default=["self"],
1240 help="The accounts to query",
1241 )
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001242
Alex Klein1699fab2022-09-08 08:46:06 -06001243 @classmethod
1244 def __call__(cls, opts):
1245 """Implement the action."""
1246 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001247
Alex Klein1699fab2022-09-08 08:46:06 -06001248 def print_one(header, data):
1249 print(f"### {header}")
1250 compact = opts.format is OutputFormat.JSON
1251 print(pformat.json(data, compact=compact).rstrip())
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001252
Alex Klein1699fab2022-09-08 08:46:06 -06001253 def task(arg):
1254 detail = gob_util.FetchUrlJson(
1255 helper.host, f"accounts/{arg}/detail"
1256 )
1257 if not detail:
1258 print(f"{arg}: account not found")
1259 else:
1260 print_one("detail", detail)
1261 for field in (
Mike Frysinger03c75072023-03-10 17:25:21 -05001262 "external.ids",
Alex Klein1699fab2022-09-08 08:46:06 -06001263 "groups",
1264 "capabilities",
1265 "preferences",
1266 "sshkeys",
1267 "gpgkeys",
1268 ):
1269 data = gob_util.FetchUrlJson(
1270 helper.host, f"accounts/{arg}/{field}"
1271 )
1272 print_one(field, data)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001273
Mike Frysinger5d615452023-08-21 10:51:32 -04001274 _run_parallel_tasks(task, opts.jobs, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -08001275
1276
Mike Frysinger2295d792021-03-08 15:55:23 -05001277class ActionConfig(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001278 """Manage the gerrit tool's own config file
Mike Frysinger2295d792021-03-08 15:55:23 -05001279
Alex Klein1699fab2022-09-08 08:46:06 -06001280 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
1281 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
Mike Frysinger2295d792021-03-08 15:55:23 -05001282
Alex Klein1699fab2022-09-08 08:46:06 -06001283 # Set up subcommand aliases.
1284 [alias]
1285 common-search = search 'is:open project:something/i/care/about'
1286 """
Mike Frysinger2295d792021-03-08 15:55:23 -05001287
Alex Klein1699fab2022-09-08 08:46:06 -06001288 COMMAND = "config"
Mike Frysinger2295d792021-03-08 15:55:23 -05001289
Alex Klein1699fab2022-09-08 08:46:06 -06001290 @staticmethod
1291 def __call__(opts):
1292 """Implement the action."""
Alex Klein4507b172023-01-13 11:39:51 -07001293 # For now, this is a place holder for raising visibility for the config
1294 # file and its associated help text documentation.
Alex Klein1699fab2022-09-08 08:46:06 -06001295 opts.parser.parse_args(["config", "--help"])
Mike Frysinger2295d792021-03-08 15:55:23 -05001296
1297
Mike Frysingere5450602021-03-08 15:34:17 -05001298class ActionHelp(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001299 """An alias to --help for CLI symmetry"""
Mike Frysingere5450602021-03-08 15:34:17 -05001300
Alex Klein1699fab2022-09-08 08:46:06 -06001301 COMMAND = "help"
1302 USE_PAGER = True
Mike Frysingere5450602021-03-08 15:34:17 -05001303
Alex Klein1699fab2022-09-08 08:46:06 -06001304 @staticmethod
1305 def init_subparser(parser):
1306 """Add arguments to this action's subparser."""
1307 parser.add_argument(
1308 "command", nargs="?", help="The command to display."
1309 )
Mike Frysingere5450602021-03-08 15:34:17 -05001310
Alex Klein1699fab2022-09-08 08:46:06 -06001311 @staticmethod
1312 def __call__(opts):
1313 """Implement the action."""
1314 # Show global help.
1315 if not opts.command:
1316 opts.parser.print_help()
1317 return
Mike Frysingere5450602021-03-08 15:34:17 -05001318
Alex Klein1699fab2022-09-08 08:46:06 -06001319 opts.parser.parse_args([opts.command, "--help"])
Mike Frysingere5450602021-03-08 15:34:17 -05001320
1321
Mike Frysinger484e2f82020-03-20 01:41:10 -04001322class ActionHelpAll(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001323 """Show all actions help output at once."""
Mike Frysinger484e2f82020-03-20 01:41:10 -04001324
Alex Klein1699fab2022-09-08 08:46:06 -06001325 COMMAND = "help-all"
1326 USE_PAGER = True
Mike Frysinger484e2f82020-03-20 01:41:10 -04001327
Alex Klein1699fab2022-09-08 08:46:06 -06001328 @staticmethod
1329 def __call__(opts):
1330 """Implement the action."""
1331 first = True
1332 for action in _GetActions():
1333 if first:
1334 first = False
1335 else:
1336 print("\n\n")
Mike Frysinger484e2f82020-03-20 01:41:10 -04001337
Alex Klein1699fab2022-09-08 08:46:06 -06001338 try:
1339 opts.parser.parse_args([action, "--help"])
1340 except SystemExit:
1341 pass
Mike Frysinger484e2f82020-03-20 01:41:10 -04001342
1343
Mike Frysinger65fc8632020-02-06 18:11:12 -05001344@memoize.Memoize
1345def _GetActions():
Alex Klein1699fab2022-09-08 08:46:06 -06001346 """Get all the possible actions we support.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001347
Alex Klein1699fab2022-09-08 08:46:06 -06001348 Returns:
Trent Apted66736d82023-05-25 10:38:28 +10001349 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1350 function that implements that command (e.g. UserActFoo).
Alex Klein1699fab2022-09-08 08:46:06 -06001351 """
1352 VALID_NAME = re.compile(r"^[a-z][a-z-]*[a-z]$")
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001353
Alex Klein1699fab2022-09-08 08:46:06 -06001354 actions = {}
1355 for cls in globals().values():
1356 if (
1357 not inspect.isclass(cls)
1358 or not issubclass(cls, UserAction)
1359 or not getattr(cls, "COMMAND", None)
1360 ):
1361 continue
Mike Frysinger65fc8632020-02-06 18:11:12 -05001362
Alex Klein1699fab2022-09-08 08:46:06 -06001363 # Sanity check names for devs adding new commands. Should be quick.
1364 cmd = cls.COMMAND
1365 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1366 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001367
Alex Klein1699fab2022-09-08 08:46:06 -06001368 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001369
Alex Klein1699fab2022-09-08 08:46:06 -06001370 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001371
1372
Harry Cutts26076b32019-02-26 15:01:29 -08001373def _GetActionUsages():
Alex Klein1699fab2022-09-08 08:46:06 -06001374 """Formats a one-line usage and doc message for each action."""
1375 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001376
Alex Klein1699fab2022-09-08 08:46:06 -06001377 cmds = list(actions.keys())
1378 functions = list(actions.values())
1379 usages = [getattr(x, "usage", "") for x in functions]
1380 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001381
Alex Klein1699fab2022-09-08 08:46:06 -06001382 cmd_indent = len(max(cmds, key=len))
1383 usage_indent = len(max(usages, key=len))
1384 return "\n".join(
1385 " %-*s %-*s : %s" % (cmd_indent, cmd, usage_indent, usage, doc)
1386 for cmd, usage, doc in zip(cmds, usages, docs)
1387 )
Harry Cutts26076b32019-02-26 15:01:29 -08001388
1389
Mike Frysinger2295d792021-03-08 15:55:23 -05001390def _AddCommonOptions(parser, subparser):
Alex Klein1699fab2022-09-08 08:46:06 -06001391 """Add options that should work before & after the subcommand.
Mike Frysinger2295d792021-03-08 15:55:23 -05001392
Alex Klein1699fab2022-09-08 08:46:06 -06001393 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1394 """
1395 parser.add_common_argument_to_group(
1396 subparser,
1397 "--ne",
1398 "--no-emails",
1399 dest="notify",
1400 default="ALL",
1401 action="store_const",
1402 const="NONE",
1403 help="Do not send e-mail notifications",
1404 )
1405 parser.add_common_argument_to_group(
1406 subparser,
1407 "-n",
1408 "--dry-run",
1409 dest="dryrun",
1410 default=False,
1411 action="store_true",
1412 help="Show what would be done, but do not make changes",
1413 )
Mike Frysinger5d615452023-08-21 10:51:32 -04001414 parser.add_common_argument_to_group(
1415 subparser,
1416 "-j",
1417 "--jobs",
1418 type=int,
1419 default=CONNECTION_LIMIT,
1420 help=(
1421 "Number of connections to run in parallel. "
1422 f"(default: {CONNECTION_LIMIT})"
1423 ),
1424 )
Mike Frysinger2295d792021-03-08 15:55:23 -05001425
1426
1427def GetBaseParser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001428 """Returns the common parser (i.e. no subparsers added)."""
1429 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001430There is no support for doing line-by-line code review via the command line.
1431This helps you manage various bits and CL status.
1432
Mike Frysingera1db2c42014-06-15 00:42:48 -07001433For general Gerrit documentation, see:
1434 https://gerrit-review.googlesource.com/Documentation/
1435The Searching Changes page covers the search query syntax:
1436 https://gerrit-review.googlesource.com/Documentation/user-search.html
1437
Mike Frysinger13f23a42013-05-13 17:32:01 -04001438Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001439 $ gerrit todo # List all the CLs that await your review.
1440 $ gerrit mine # List all of your open CLs.
1441 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1442 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1443 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001444 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
144528123.
1446 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1447CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001448Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001449 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1450with Commit-Queue=1.
1451 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1452CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001453 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001454
Harry Cutts26076b32019-02-26 15:01:29 -08001455Actions:
1456"""
Alex Klein1699fab2022-09-08 08:46:06 -06001457 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001458
Alex Klein1699fab2022-09-08 08:46:06 -06001459 site_params = config_lib.GetSiteParams()
1460 parser = commandline.ArgumentParser(
1461 description=description,
1462 default_log_level="notice",
1463 epilog="For subcommand help, use `gerrit help <command>`.",
1464 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001465
Alex Klein1699fab2022-09-08 08:46:06 -06001466 group = parser.add_argument_group("Server options")
1467 group.add_argument(
1468 "-i",
1469 "--internal",
1470 dest="gob",
1471 action="store_const",
1472 default=site_params.EXTERNAL_GOB_INSTANCE,
1473 const=site_params.INTERNAL_GOB_INSTANCE,
1474 help="Query internal Chrome Gerrit instance",
1475 )
1476 group.add_argument(
1477 "-g",
1478 "--gob",
1479 default=site_params.EXTERNAL_GOB_INSTANCE,
Trent Apted66736d82023-05-25 10:38:28 +10001480 help="Gerrit (on borg) instance to query (default: %(default)s)",
Alex Klein1699fab2022-09-08 08:46:06 -06001481 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001482
Alex Klein1699fab2022-09-08 08:46:06 -06001483 group = parser.add_argument_group("CL options")
1484 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001485
Alex Klein1699fab2022-09-08 08:46:06 -06001486 group = parser.add_mutually_exclusive_group()
1487 parser.set_defaults(format=OutputFormat.AUTO)
1488 group.add_argument(
1489 "--format",
1490 action="enum",
1491 enum=OutputFormat,
1492 help="Output format to use.",
1493 )
1494 group.add_argument(
1495 "--raw",
1496 action="store_const",
1497 dest="format",
1498 const=OutputFormat.RAW,
1499 help="Alias for --format=raw.",
1500 )
1501 group.add_argument(
1502 "--json",
1503 action="store_const",
1504 dest="format",
1505 const=OutputFormat.JSON,
1506 help="Alias for --format=json.",
1507 )
Jack Rosenthal95aac172022-06-30 15:35:07 -06001508
Alex Klein1699fab2022-09-08 08:46:06 -06001509 group = parser.add_mutually_exclusive_group()
1510 group.add_argument(
1511 "--pager",
1512 action="store_true",
1513 default=sys.stdout.isatty(),
1514 help="Enable pager.",
1515 )
1516 group.add_argument(
1517 "--no-pager", action="store_false", dest="pager", help="Disable pager."
1518 )
1519 return parser
Mike Frysinger2295d792021-03-08 15:55:23 -05001520
1521
Alex Klein1699fab2022-09-08 08:46:06 -06001522def GetParser(
1523 parser: commandline.ArgumentParser = None,
Mike Frysinger16474792023-03-01 01:18:00 -05001524) -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001525 """Returns the full parser to use for this module."""
1526 if parser is None:
1527 parser = GetBaseParser()
Mike Frysinger2295d792021-03-08 15:55:23 -05001528
Alex Klein1699fab2022-09-08 08:46:06 -06001529 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001530
Alex Klein1699fab2022-09-08 08:46:06 -06001531 # Subparsers are required by default under Python 2. Python 3 changed to
1532 # not required, but didn't include a required option until 3.7. Setting
1533 # the required member works in all versions (and setting dest name).
1534 subparsers = parser.add_subparsers(dest="action")
1535 subparsers.required = True
1536 for cmd, cls in actions.items():
1537 # Format the full docstring by removing the file level indentation.
1538 description = re.sub(r"^ ", "", cls.__doc__, flags=re.M)
1539 subparser = subparsers.add_parser(cmd, description=description)
1540 _AddCommonOptions(parser, subparser)
1541 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001542
Alex Klein1699fab2022-09-08 08:46:06 -06001543 return parser
Mike Frysinger108eda22018-06-06 18:45:12 -04001544
1545
Jack Rosenthal95aac172022-06-30 15:35:07 -06001546def start_pager():
Alex Klein1699fab2022-09-08 08:46:06 -06001547 """Re-spawn ourselves attached to a pager."""
1548 pager = os.environ.get("PAGER", "less")
1549 os.environ.setdefault("LESS", "FRX")
Jack Rosenthal95aac172022-06-30 15:35:07 -06001550 with subprocess.Popen(
Alex Klein1699fab2022-09-08 08:46:06 -06001551 # sys.argv can have some edge cases: we may not necessarily use
1552 # sys.executable if the script is executed as "python path/to/script".
1553 # If we upgrade to Python 3.10+, this should be changed to sys.orig_argv
1554 # for full accuracy.
1555 sys.argv,
1556 stdout=subprocess.PIPE,
1557 stderr=subprocess.STDOUT,
1558 env={"GERRIT_RESPAWN_FOR_PAGER": "1", **os.environ},
1559 ) as gerrit_proc:
1560 with subprocess.Popen(
1561 pager,
1562 shell=True,
1563 stdin=gerrit_proc.stdout,
1564 ) as pager_proc:
1565 # Send SIGINT to just the gerrit process, not the pager too.
1566 def _sighandler(signum, _frame):
1567 gerrit_proc.send_signal(signum)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001568
Alex Klein1699fab2022-09-08 08:46:06 -06001569 signal.signal(signal.SIGINT, _sighandler)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001570
Alex Klein1699fab2022-09-08 08:46:06 -06001571 pager_proc.communicate()
1572 # If the pager exits, and the gerrit process is still running, we
1573 # must terminate it.
1574 if gerrit_proc.poll() is None:
1575 gerrit_proc.terminate()
1576 sys.exit(gerrit_proc.wait())
Jack Rosenthal95aac172022-06-30 15:35:07 -06001577
1578
Mike Frysinger108eda22018-06-06 18:45:12 -04001579def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -06001580 base_parser = GetBaseParser()
1581 opts, subargs = base_parser.parse_known_args(argv)
Mike Frysinger2295d792021-03-08 15:55:23 -05001582
Alex Klein1699fab2022-09-08 08:46:06 -06001583 config = Config()
1584 if subargs:
Alex Klein4507b172023-01-13 11:39:51 -07001585 # If the action is an alias to an expanded value, we need to mutate the
1586 # argv and reparse things.
Alex Klein1699fab2022-09-08 08:46:06 -06001587 action = config.expand_alias(subargs[0])
1588 if action != subargs[0]:
1589 pos = argv.index(subargs[0])
1590 argv = argv[:pos] + action + argv[pos + 1 :]
Mike Frysinger2295d792021-03-08 15:55:23 -05001591
Alex Klein1699fab2022-09-08 08:46:06 -06001592 parser = GetParser(parser=base_parser)
1593 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001594
Alex Klein1699fab2022-09-08 08:46:06 -06001595 # If we're running as a re-spawn for the pager, from this point on
1596 # we'll pretend we're attached to a TTY. This will give us colored
1597 # output when requested.
1598 if os.environ.pop("GERRIT_RESPAWN_FOR_PAGER", None) is not None:
1599 opts.pager = False
1600 sys.stdout.isatty = lambda: True
Jack Rosenthal95aac172022-06-30 15:35:07 -06001601
Alex Klein1699fab2022-09-08 08:46:06 -06001602 # In case the action wants to throw a parser error.
1603 opts.parser = parser
Mike Frysinger484e2f82020-03-20 01:41:10 -04001604
Alex Klein1699fab2022-09-08 08:46:06 -06001605 # A cache of gerrit helpers we'll load on demand.
1606 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001607
Alex Klein1699fab2022-09-08 08:46:06 -06001608 if opts.format is OutputFormat.AUTO:
1609 if sys.stdout.isatty():
1610 opts.format = OutputFormat.PRETTY
1611 else:
1612 opts.format = OutputFormat.RAW
Jack Rosenthale3a92672022-06-29 14:54:48 -06001613
Alex Klein1699fab2022-09-08 08:46:06 -06001614 opts.Freeze()
Mike Frysinger88f27292014-06-17 09:40:45 -07001615
Alex Klein1699fab2022-09-08 08:46:06 -06001616 # pylint: disable=global-statement
1617 global COLOR
1618 COLOR = terminal.Color(enabled=opts.color)
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001619
Alex Klein1699fab2022-09-08 08:46:06 -06001620 # Now look up the requested user action and run it.
1621 actions = _GetActions()
1622 action_class = actions[opts.action]
1623 if action_class.USE_PAGER and opts.pager:
1624 start_pager()
1625 obj = action_class()
1626 try:
1627 obj(opts)
1628 except (
1629 cros_build_lib.RunCommandError,
1630 gerrit.GerritException,
1631 gob_util.GOBError,
1632 ) as e:
1633 cros_build_lib.Die(e)