blob: 1fa4fbd6a2a7a870ad44396bf598d30e989bf7c0 [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
Mike Frysingerc7796cf2020-02-06 23:55:15 -050064class UserAction(object):
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 Frysinger254f33f2019-12-11 13:54:29 -0500166def _run_parallel_tasks(task, *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
Alex Klein1699fab2022-09-08 08:46:06 -0600185 with parallel.BackgroundTaskRunner(retry, processes=CONNECTION_LIMIT) as q:
186 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:
213 items: Items that begin with "~" mean "remove" while others are "add".
214 validate: A regular expression to validate each item.
215
216 Returns:
217 A tuple of sets: all the items to add and all the items to remove.
218 NB: The leading "~" will automatically be stripped.
219 """
220 validator = re.compile(validate) if validate else None
221
222 add_list, remove_list, invalid_list = set(), set(), set()
223 for item in items:
224 if not item:
225 invalid_list.add(item)
226 continue
227
228 remove = False
229 if item[0] == "~":
230 remove = True
231 item = item[1:]
232
233 if validator and not validator.match(item):
234 invalid_list.add(item)
235 elif remove:
236 remove_list.add(item)
237 add_list.discard(item)
238 else:
239 add_list.add(item)
240 remove_list.discard(item)
241
242 if invalid_list:
243 cros_build_lib.Die("Invalid arguments: %s", ", ".join(invalid_list))
244
245 return (add_list, remove_list)
246
247
Mike Frysinger88f27292014-06-17 09:40:45 -0700248# TODO: This func really needs to be merged into the core gerrit logic.
249def GetGerrit(opts, cl=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600250 """Auto pick the right gerrit instance based on the |cl|
Mike Frysinger88f27292014-06-17 09:40:45 -0700251
Alex Klein1699fab2022-09-08 08:46:06 -0600252 Args:
Trent Apted66736d82023-05-25 10:38:28 +1000253 opts: The general options object.
254 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
Mike Frysinger88f27292014-06-17 09:40:45 -0700255
Alex Klein1699fab2022-09-08 08:46:06 -0600256 Returns:
Trent Apted66736d82023-05-25 10:38:28 +1000257 A tuple of a gerrit object and a sanitized CL #.
Alex Klein1699fab2022-09-08 08:46:06 -0600258 """
259 gob = opts.gob
260 if cl is not None:
261 if cl.startswith("*") or cl.startswith("chrome-internal:"):
262 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
263 if cl.startswith("*"):
264 cl = cl[1:]
265 else:
266 cl = cl[16:]
267 elif ":" in cl:
268 gob, cl = cl.split(":", 1)
Mike Frysinger88f27292014-06-17 09:40:45 -0700269
Alex Klein1699fab2022-09-08 08:46:06 -0600270 if not gob in opts.gerrit:
271 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
Mike Frysinger88f27292014-06-17 09:40:45 -0700272
Alex Klein1699fab2022-09-08 08:46:06 -0600273 return (opts.gerrit[gob], cl)
Mike Frysinger88f27292014-06-17 09:40:45 -0700274
275
Mike Frysinger13f23a42013-05-13 17:32:01 -0400276def GetApprovalSummary(_opts, cls):
Alex Klein1699fab2022-09-08 08:46:06 -0600277 """Return a dict of the most important approvals"""
Alex Kleine37b8762023-04-17 12:16:15 -0600278 approvs = {x: "" for x in GERRIT_SUMMARY_CATS}
Alex Klein1699fab2022-09-08 08:46:06 -0600279 for approver in cls.get("currentPatchSet", {}).get("approvals", []):
280 cats = GERRIT_APPROVAL_MAP.get(approver["type"])
281 if not cats:
282 logging.warning(
283 "unknown gerrit approval type: %s", approver["type"]
284 )
285 continue
286 cat = cats[0].strip()
287 val = int(approver["value"])
288 if not cat in approvs:
289 # Ignore the extended categories in the summary view.
290 continue
291 elif approvs[cat] == "":
292 approvs[cat] = val
293 elif val < 0:
294 approvs[cat] = min(approvs[cat], val)
295 else:
296 approvs[cat] = max(approvs[cat], val)
297 return approvs
Mike Frysinger13f23a42013-05-13 17:32:01 -0400298
299
Mike Frysingera1b4b272017-04-05 16:11:00 -0400300def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600301 """Pretty print a single result"""
302 if lims is None:
303 lims = {"url": 0, "project": 0}
Mike Frysinger13f23a42013-05-13 17:32:01 -0400304
Alex Klein1699fab2022-09-08 08:46:06 -0600305 status = ""
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400306
Alex Klein1699fab2022-09-08 08:46:06 -0600307 if opts.verbose:
308 status += "%s " % (cl["status"],)
309 else:
310 status += "%s " % (GERRIT_SUMMARY_MAP.get(cl["status"], cl["status"]),)
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400311
Alex Klein1699fab2022-09-08 08:46:06 -0600312 if show_approvals and not opts.verbose:
313 approvs = GetApprovalSummary(opts, cl)
314 for cat in GERRIT_SUMMARY_CATS:
315 if approvs[cat] in ("", 0):
316 functor = lambda x: x
317 elif approvs[cat] < 0:
318 functor = red
319 else:
320 functor = green
321 status += functor("%s:%2s " % (cat, approvs[cat]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400322
Alex Klein1699fab2022-09-08 08:46:06 -0600323 if opts.format is OutputFormat.MARKDOWN:
324 print("* %s - %s" % (uri_lib.ShortenUri(cl["url"]), cl["subject"]))
325 else:
326 print(
327 "%s %s%-*s %s"
328 % (
329 blue("%-*s" % (lims["url"], cl["url"])),
330 status,
331 lims["project"],
332 cl["project"],
333 cl["subject"],
334 )
335 )
Mike Frysinger13f23a42013-05-13 17:32:01 -0400336
Alex Klein1699fab2022-09-08 08:46:06 -0600337 if show_approvals and opts.verbose:
338 for approver in cl["currentPatchSet"].get("approvals", []):
339 functor = red if int(approver["value"]) < 0 else green
340 n = functor("%2s" % approver["value"])
341 t = GERRIT_APPROVAL_MAP.get(
342 approver["type"], [approver["type"], approver["type"]]
343 )[1]
344 print(" %s %s %s" % (n, t, approver["by"]["email"]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400345
346
Mike Frysingera1b4b272017-04-05 16:11:00 -0400347def PrintCls(opts, cls, lims=None, show_approvals=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600348 """Print all results based on the requested format."""
349 if opts.format is OutputFormat.RAW:
350 site_params = config_lib.GetSiteParams()
351 pfx = ""
352 # Special case internal Chrome GoB as that is what most devs use.
353 # They can always redirect the list elsewhere via the -g option.
354 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
355 pfx = site_params.INTERNAL_CHANGE_PREFIX
356 for cl in cls:
357 print("%s%s" % (pfx, cl["number"]))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400358
Alex Klein1699fab2022-09-08 08:46:06 -0600359 elif opts.format is OutputFormat.JSON:
360 json.dump(cls, sys.stdout)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400361
Alex Klein1699fab2022-09-08 08:46:06 -0600362 else:
363 if lims is None:
364 lims = limits(cls)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400365
Alex Klein1699fab2022-09-08 08:46:06 -0600366 for cl in cls:
367 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400368
369
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400370def _Query(opts, query, raw=True, helper=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600371 """Queries Gerrit with a query string built from the commandline options"""
372 if opts.branch is not None:
373 query += " branch:%s" % opts.branch
374 if opts.project is not None:
375 query += " project: %s" % opts.project
376 if opts.topic is not None:
377 query += " topic: %s" % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800378
Alex Klein1699fab2022-09-08 08:46:06 -0600379 if helper is None:
380 helper, _ = GetGerrit(opts)
381 return helper.Query(query, raw=raw, bypass_cache=False)
Paul Hobbs89765232015-06-24 14:07:49 -0700382
383
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400384def FilteredQuery(opts, query, helper=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600385 """Query gerrit and filter/clean up the results"""
386 ret = []
Paul Hobbs89765232015-06-24 14:07:49 -0700387
Alex Klein1699fab2022-09-08 08:46:06 -0600388 logging.debug("Running query: %s", query)
389 for cl in _Query(opts, query, raw=True, helper=helper):
390 # Gerrit likes to return a stats record too.
391 if not "project" in cl:
392 continue
Mike Frysinger13f23a42013-05-13 17:32:01 -0400393
Alex Klein1699fab2022-09-08 08:46:06 -0600394 # Strip off common leading names since the result is still
395 # unique over the whole tree.
396 if not opts.verbose:
397 for pfx in (
398 "aosp",
399 "chromeos",
400 "chromiumos",
401 "external",
402 "overlays",
403 "platform",
404 "third_party",
405 ):
406 if cl["project"].startswith("%s/" % pfx):
407 cl["project"] = cl["project"][len(pfx) + 1 :]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400408
Alex Klein1699fab2022-09-08 08:46:06 -0600409 cl["url"] = uri_lib.ShortenUri(cl["url"])
Mike Frysinger479f1192017-09-14 22:36:30 -0400410
Alex Klein1699fab2022-09-08 08:46:06 -0600411 ret.append(cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400412
Alex Klein1699fab2022-09-08 08:46:06 -0600413 if opts.sort == "unsorted":
414 return ret
415 if opts.sort == "number":
416 key = lambda x: int(x[opts.sort])
417 else:
418 key = lambda x: x[opts.sort]
419 return sorted(ret, key=key)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400420
421
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500422class _ActionSearchQuery(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600423 """Base class for actions that perform searches."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500424
Alex Klein1699fab2022-09-08 08:46:06 -0600425 USE_PAGER = True
Jack Rosenthal95aac172022-06-30 15:35:07 -0600426
Alex Klein1699fab2022-09-08 08:46:06 -0600427 @staticmethod
428 def init_subparser(parser):
429 """Add arguments to this action's subparser."""
430 parser.add_argument(
431 "--sort",
432 default="number",
Trent Apted66736d82023-05-25 10:38:28 +1000433 help='Key to sort on (number, project); use "unsorted" to disable',
Alex Klein1699fab2022-09-08 08:46:06 -0600434 )
435 parser.add_argument(
436 "-b", "--branch", help="Limit output to the specific branch"
437 )
438 parser.add_argument(
439 "-p", "--project", help="Limit output to the specific project"
440 )
441 parser.add_argument(
442 "-t", "--topic", help="Limit output to the specific topic"
443 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500444
445
446class ActionTodo(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600447 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500448
Alex Klein1699fab2022-09-08 08:46:06 -0600449 COMMAND = "todo"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500450
Alex Klein1699fab2022-09-08 08:46:06 -0600451 @staticmethod
452 def __call__(opts):
453 """Implement the action."""
454 cls = FilteredQuery(opts, "attention:self")
455 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400456
457
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500458class ActionSearch(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600459 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500460
Alex Klein1699fab2022-09-08 08:46:06 -0600461 COMMAND = "search"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500462
Alex Klein1699fab2022-09-08 08:46:06 -0600463 @staticmethod
464 def init_subparser(parser):
465 """Add arguments to this action's subparser."""
466 _ActionSearchQuery.init_subparser(parser)
467 parser.add_argument("query", help="The search query")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500468
Alex Klein1699fab2022-09-08 08:46:06 -0600469 @staticmethod
470 def __call__(opts):
471 """Implement the action."""
472 cls = FilteredQuery(opts, opts.query)
473 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400474
475
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500476class ActionMine(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600477 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500478
Alex Klein1699fab2022-09-08 08:46:06 -0600479 COMMAND = "mine"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500480
Alex Klein1699fab2022-09-08 08:46:06 -0600481 @staticmethod
482 def init_subparser(parser):
483 """Add arguments to this action's subparser."""
484 _ActionSearchQuery.init_subparser(parser)
485 parser.add_argument(
486 "--draft",
487 default=False,
488 action="store_true",
489 help="Show draft changes",
490 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500491
Alex Klein1699fab2022-09-08 08:46:06 -0600492 @staticmethod
493 def __call__(opts):
494 """Implement the action."""
495 if opts.draft:
496 rule = "is:draft"
497 else:
498 rule = "status:new"
499 cls = FilteredQuery(opts, "owner:self %s" % (rule,))
500 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700501
502
Paul Hobbs89765232015-06-24 14:07:49 -0700503def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
Alex Klein1699fab2022-09-08 08:46:06 -0600504 """Runs breadth first search starting from the nodes in |to_visit|
Paul Hobbs89765232015-06-24 14:07:49 -0700505
Alex Klein1699fab2022-09-08 08:46:06 -0600506 Args:
Trent Apted66736d82023-05-25 10:38:28 +1000507 to_visit: the starting nodes
508 children: a function which takes a node and returns the adjacent nodes
509 visited_key: a function for deduplicating node visits. Defaults to the
510 identity function (lambda x: x)
Paul Hobbs89765232015-06-24 14:07:49 -0700511
Alex Klein1699fab2022-09-08 08:46:06 -0600512 Returns:
Trent Apted66736d82023-05-25 10:38:28 +1000513 A list of nodes which are reachable from any node in |to_visit| by
514 calling
515 |children| any number of times.
Alex Klein1699fab2022-09-08 08:46:06 -0600516 """
517 to_visit = list(to_visit)
518 seen = set(visited_key(x) for x in to_visit)
519 for node in to_visit:
520 for child in children(node):
521 key = visited_key(child)
522 if key not in seen:
523 seen.add(key)
524 to_visit.append(child)
525 return to_visit
Paul Hobbs89765232015-06-24 14:07:49 -0700526
527
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500528class ActionDeps(_ActionSearchQuery):
Alex Klein4507b172023-01-13 11:39:51 -0700529 """List CLs matching a query, and transitive dependencies of those CLs."""
Paul Hobbs89765232015-06-24 14:07:49 -0700530
Alex Klein1699fab2022-09-08 08:46:06 -0600531 COMMAND = "deps"
Paul Hobbs89765232015-06-24 14:07:49 -0700532
Alex Klein1699fab2022-09-08 08:46:06 -0600533 @staticmethod
534 def init_subparser(parser):
535 """Add arguments to this action's subparser."""
536 _ActionSearchQuery.init_subparser(parser)
537 parser.add_argument("query", help="The search query")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500538
Alex Klein1699fab2022-09-08 08:46:06 -0600539 def __call__(self, opts):
540 """Implement the action."""
541 cls = _Query(opts, opts.query, raw=False)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500542
Alex Klein1699fab2022-09-08 08:46:06 -0600543 @memoize.Memoize
544 def _QueryChange(cl, helper=None):
545 return _Query(opts, cl, raw=False, helper=helper)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500546
Alex Klein1699fab2022-09-08 08:46:06 -0600547 transitives = _BreadthFirstSearch(
548 cls,
549 functools.partial(self._Children, opts, _QueryChange),
550 visited_key=lambda cl: cl.PatchLink(),
551 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500552
Alex Klein1699fab2022-09-08 08:46:06 -0600553 # This is a hack to avoid losing GoB host for each CL. The PrintCls
554 # function assumes the GoB host specified by the user is the only one
555 # that is ever used, but the deps command walks across hosts.
556 if opts.format is OutputFormat.RAW:
557 print("\n".join(x.PatchLink() for x in transitives))
558 else:
559 transitives_raw = [cl.patch_dict for cl in transitives]
560 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500561
Alex Klein1699fab2022-09-08 08:46:06 -0600562 @staticmethod
563 def _ProcessDeps(opts, querier, cl, deps, required):
564 """Yields matching dependencies for a patch"""
565 # We need to query the change to guarantee that we have a .gerrit_number
566 for dep in deps:
567 if not dep.remote in opts.gerrit:
568 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
569 remote=dep.remote, print_cmd=opts.debug
570 )
571 helper = opts.gerrit[dep.remote]
Mike Frysingerb3300c42017-07-20 01:41:17 -0400572
Alex Klein1699fab2022-09-08 08:46:06 -0600573 # TODO(phobbs) this should maybe catch network errors.
574 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400575
Alex Klein4507b172023-01-13 11:39:51 -0700576 # Handle empty results. If we found a commit that was pushed
577 # directly (e.g. a bot commit), then gerrit won't know about it.
Alex Klein1699fab2022-09-08 08:46:06 -0600578 if not changes:
579 if required:
580 logging.error(
581 "CL %s depends on %s which cannot be found",
582 cl,
583 dep.ToGerritQueryText(),
584 )
585 continue
Mike Frysinger5726da92017-09-20 22:14:25 -0400586
Alex Klein4507b172023-01-13 11:39:51 -0700587 # Our query might have matched more than one result. This can come
588 # up when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id
589 # shows up across multiple repos/branches. We blindly check all of
590 # them in the hopes that all open ones are what the user wants, but
591 # then again the CQ-DEPEND syntax itself is unable to differentiate.
592 # *shrug*
Alex Klein1699fab2022-09-08 08:46:06 -0600593 if len(changes) > 1:
594 logging.warning(
595 "CL %s has an ambiguous CQ dependency %s",
596 cl,
597 dep.ToGerritQueryText(),
598 )
599 for change in changes:
600 if change.status == "NEW":
601 yield change
Mike Frysinger5726da92017-09-20 22:14:25 -0400602
Alex Klein1699fab2022-09-08 08:46:06 -0600603 @classmethod
604 def _Children(cls, opts, querier, cl):
605 """Yields the Gerrit dependencies of a patch"""
606 for change in cls._ProcessDeps(
607 opts, querier, cl, cl.GerritDependencies(), False
608 ):
609 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700610
Paul Hobbs89765232015-06-24 14:07:49 -0700611
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500612class ActionInspect(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600613 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500614
Alex Klein1699fab2022-09-08 08:46:06 -0600615 COMMAND = "inspect"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500616
Alex Klein1699fab2022-09-08 08:46:06 -0600617 @staticmethod
618 def init_subparser(parser):
619 """Add arguments to this action's subparser."""
620 _ActionSearchQuery.init_subparser(parser)
621 parser.add_argument(
622 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
623 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500624
Alex Klein1699fab2022-09-08 08:46:06 -0600625 @staticmethod
626 def __call__(opts):
627 """Implement the action."""
628 cls = []
629 for arg in opts.cls:
630 helper, cl = GetGerrit(opts, arg)
631 change = FilteredQuery(opts, "change:%s" % cl, helper=helper)
632 if change:
633 cls.extend(change)
634 else:
635 logging.warning("no results found for CL %s", arg)
636 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400637
638
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500639class _ActionLabeler(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600640 """Base helper for setting labels."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500641
Alex Klein1699fab2022-09-08 08:46:06 -0600642 LABEL = None
643 VALUES = None
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500644
Alex Klein1699fab2022-09-08 08:46:06 -0600645 @classmethod
646 def init_subparser(cls, parser):
647 """Add arguments to this action's subparser."""
648 parser.add_argument(
649 "-m",
650 "--msg",
651 "--message",
652 metavar="MESSAGE",
653 help="Optional message to include",
654 )
655 parser.add_argument(
656 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
657 )
658 parser.add_argument(
659 "value",
660 nargs=1,
661 metavar="value",
662 choices=cls.VALUES,
663 help="The label value; one of [%(choices)s]",
664 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500665
Alex Klein1699fab2022-09-08 08:46:06 -0600666 @classmethod
667 def __call__(cls, opts):
668 """Implement the action."""
Mike Frysinger16474792023-03-01 01:18:00 -0500669
Alex Klein1699fab2022-09-08 08:46:06 -0600670 # Convert user-friendly command line option into a gerrit parameter.
671 def task(arg):
672 helper, cl = GetGerrit(opts, arg)
673 helper.SetReview(
674 cl,
675 labels={cls.LABEL: opts.value[0]},
676 msg=opts.msg,
677 dryrun=opts.dryrun,
678 notify=opts.notify,
679 )
680
681 _run_parallel_tasks(task, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500682
683
684class ActionLabelAutoSubmit(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600685 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500686
Alex Klein1699fab2022-09-08 08:46:06 -0600687 COMMAND = "label-as"
688 LABEL = "Auto-Submit"
689 VALUES = ("0", "1")
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600690
691
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500692class ActionLabelCodeReview(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600693 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500694
Alex Klein1699fab2022-09-08 08:46:06 -0600695 COMMAND = "label-cr"
696 LABEL = "Code-Review"
697 VALUES = ("-2", "-1", "0", "1", "2")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400698
699
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500700class ActionLabelVerified(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600701 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500702
Alex Klein1699fab2022-09-08 08:46:06 -0600703 COMMAND = "label-v"
704 LABEL = "Verified"
705 VALUES = ("-1", "0", "1")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400706
707
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500708class ActionLabelCommitQueue(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600709 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500710
Alex Klein1699fab2022-09-08 08:46:06 -0600711 COMMAND = "label-cq"
712 LABEL = "Commit-Queue"
713 VALUES = ("0", "1", "2")
714
Mike Frysinger15b23e42014-12-05 17:00:05 -0500715
C Shapiro3f1f8242021-08-02 15:28:29 -0500716class ActionLabelOwnersOverride(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600717 """Change the Owners-Override label (1=Override)"""
C Shapiro3f1f8242021-08-02 15:28:29 -0500718
Alex Klein1699fab2022-09-08 08:46:06 -0600719 COMMAND = "label-oo"
720 LABEL = "Owners-Override"
721 VALUES = ("0", "1")
C Shapiro3f1f8242021-08-02 15:28:29 -0500722
Mike Frysinger15b23e42014-12-05 17:00:05 -0500723
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500724class _ActionSimpleParallelCLs(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600725 """Base helper for actions that only accept CLs."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500726
Alex Klein1699fab2022-09-08 08:46:06 -0600727 @staticmethod
728 def init_subparser(parser):
729 """Add arguments to this action's subparser."""
730 parser.add_argument(
731 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
732 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500733
Alex Klein1699fab2022-09-08 08:46:06 -0600734 def __call__(self, opts):
735 """Implement the action."""
736
737 def task(arg):
738 helper, cl = GetGerrit(opts, arg)
739 self._process_one(helper, cl, opts)
740
741 _run_parallel_tasks(task, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500742
743
744class ActionSubmit(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600745 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500746
Alex Klein1699fab2022-09-08 08:46:06 -0600747 COMMAND = "submit"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500748
Alex Klein1699fab2022-09-08 08:46:06 -0600749 @staticmethod
750 def _process_one(helper, cl, opts):
751 """Use |helper| to process the single |cl|."""
752 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400753
754
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500755class ActionAbandon(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600756 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500757
Alex Klein1699fab2022-09-08 08:46:06 -0600758 COMMAND = "abandon"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500759
Alex Klein1699fab2022-09-08 08:46:06 -0600760 @staticmethod
761 def init_subparser(parser):
762 """Add arguments to this action's subparser."""
763 parser.add_argument(
764 "-m",
765 "--msg",
766 "--message",
767 metavar="MESSAGE",
768 help="Include a message",
769 )
770 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger3af378b2021-03-12 01:34:04 -0500771
Alex Klein1699fab2022-09-08 08:46:06 -0600772 @staticmethod
773 def _process_one(helper, cl, opts):
774 """Use |helper| to process the single |cl|."""
775 helper.AbandonChange(
776 cl, msg=opts.msg, dryrun=opts.dryrun, notify=opts.notify
777 )
Mike Frysinger13f23a42013-05-13 17:32:01 -0400778
779
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500780class ActionRestore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600781 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500782
Alex Klein1699fab2022-09-08 08:46:06 -0600783 COMMAND = "restore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500784
Alex Klein1699fab2022-09-08 08:46:06 -0600785 @staticmethod
786 def _process_one(helper, cl, opts):
787 """Use |helper| to process the single |cl|."""
788 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400789
790
Tomasz Figa54d70992021-01-20 13:48:59 +0900791class ActionWorkInProgress(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600792 """Mark CLs as work in progress"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900793
Alex Klein1699fab2022-09-08 08:46:06 -0600794 COMMAND = "wip"
Tomasz Figa54d70992021-01-20 13:48:59 +0900795
Alex Klein1699fab2022-09-08 08:46:06 -0600796 @staticmethod
797 def _process_one(helper, cl, opts):
798 """Use |helper| to process the single |cl|."""
799 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900800
801
802class ActionReadyForReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600803 """Mark CLs as ready for review"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900804
Alex Klein1699fab2022-09-08 08:46:06 -0600805 COMMAND = "ready"
Tomasz Figa54d70992021-01-20 13:48:59 +0900806
Alex Klein1699fab2022-09-08 08:46:06 -0600807 @staticmethod
808 def _process_one(helper, cl, opts):
809 """Use |helper| to process the single |cl|."""
810 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900811
812
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500813class ActionReviewers(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600814 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700815
Alex Klein1699fab2022-09-08 08:46:06 -0600816 COMMAND = "reviewers"
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700817
Alex Klein1699fab2022-09-08 08:46:06 -0600818 @staticmethod
819 def init_subparser(parser):
820 """Add arguments to this action's subparser."""
821 parser.add_argument("cl", metavar="CL", help="The CL to update")
822 parser.add_argument(
823 "reviewers", nargs="+", help="The reviewers to add/remove"
824 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700825
Alex Klein1699fab2022-09-08 08:46:06 -0600826 @staticmethod
827 def __call__(opts):
828 """Implement the action."""
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400829 add_list, remove_list = process_add_remove_lists(
830 opts.reviewers, f"^{constants.EMAIL_REGEX}$"
831 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500832
Alex Klein1699fab2022-09-08 08:46:06 -0600833 if add_list or remove_list:
834 helper, cl = GetGerrit(opts, opts.cl)
835 helper.SetReviewers(
836 cl,
837 add=add_list,
838 remove=remove_list,
839 dryrun=opts.dryrun,
840 notify=opts.notify,
841 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700842
843
Brian Norrisd25af082021-10-29 11:25:31 -0700844class ActionAttentionSet(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600845 """Add/remove emails from the attention set (prepend with '~' to remove)"""
Brian Norrisd25af082021-10-29 11:25:31 -0700846
Alex Klein1699fab2022-09-08 08:46:06 -0600847 COMMAND = "attention"
Brian Norrisd25af082021-10-29 11:25:31 -0700848
Alex Klein1699fab2022-09-08 08:46:06 -0600849 @staticmethod
850 def init_subparser(parser):
851 """Add arguments to this action's subparser."""
852 parser.add_argument(
853 "-m",
854 "--msg",
855 "--message",
856 metavar="MESSAGE",
857 help="Optional message to include",
858 default="gerrit CLI",
859 )
860 parser.add_argument("cl", metavar="CL", help="The CL to update")
861 parser.add_argument(
862 "users",
863 nargs="+",
864 help="The users to add/remove from attention set",
865 )
Brian Norrisd25af082021-10-29 11:25:31 -0700866
Alex Klein1699fab2022-09-08 08:46:06 -0600867 @staticmethod
868 def __call__(opts):
869 """Implement the action."""
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400870 add_list, remove_list = process_add_remove_lists(
871 opts.users, f"^{constants.EMAIL_REGEX}$"
872 )
Brian Norrisd25af082021-10-29 11:25:31 -0700873
Alex Klein1699fab2022-09-08 08:46:06 -0600874 if add_list or remove_list:
875 helper, cl = GetGerrit(opts, opts.cl)
876 helper.SetAttentionSet(
877 cl,
878 add=add_list,
879 remove=remove_list,
880 dryrun=opts.dryrun,
881 notify=opts.notify,
882 message=opts.msg,
883 )
Brian Norrisd25af082021-10-29 11:25:31 -0700884
885
Mike Frysinger62178ae2020-03-20 01:37:43 -0400886class ActionMessage(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600887 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500888
Alex Klein1699fab2022-09-08 08:46:06 -0600889 COMMAND = "message"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500890
Alex Klein1699fab2022-09-08 08:46:06 -0600891 @staticmethod
892 def init_subparser(parser):
893 """Add arguments to this action's subparser."""
894 _ActionSimpleParallelCLs.init_subparser(parser)
895 parser.add_argument("message", help="The message to post")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500896
Alex Klein1699fab2022-09-08 08:46:06 -0600897 @staticmethod
898 def _process_one(helper, cl, opts):
899 """Use |helper| to process the single |cl|."""
900 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530901
902
Mike Frysinger62178ae2020-03-20 01:37:43 -0400903class ActionTopic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600904 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500905
Alex Klein1699fab2022-09-08 08:46:06 -0600906 COMMAND = "topic"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500907
Alex Klein1699fab2022-09-08 08:46:06 -0600908 @staticmethod
909 def init_subparser(parser):
910 """Add arguments to this action's subparser."""
911 _ActionSimpleParallelCLs.init_subparser(parser)
912 parser.add_argument("topic", help="The topic to set")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500913
Alex Klein1699fab2022-09-08 08:46:06 -0600914 @staticmethod
915 def _process_one(helper, cl, opts):
916 """Use |helper| to process the single |cl|."""
917 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800918
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800919
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500920class ActionPrivate(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600921 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700922
Alex Klein1699fab2022-09-08 08:46:06 -0600923 COMMAND = "private"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500924
Alex Klein1699fab2022-09-08 08:46:06 -0600925 @staticmethod
926 def _process_one(helper, cl, opts):
927 """Use |helper| to process the single |cl|."""
928 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700929
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800930
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500931class ActionPublic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600932 """Mark CLs public"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500933
Alex Klein1699fab2022-09-08 08:46:06 -0600934 COMMAND = "public"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500935
Alex Klein1699fab2022-09-08 08:46:06 -0600936 @staticmethod
937 def _process_one(helper, cl, opts):
938 """Use |helper| to process the single |cl|."""
939 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500940
941
942class ActionSethashtags(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600943 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500944
Alex Klein1699fab2022-09-08 08:46:06 -0600945 COMMAND = "hashtags"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500946
Alex Klein1699fab2022-09-08 08:46:06 -0600947 @staticmethod
948 def init_subparser(parser):
949 """Add arguments to this action's subparser."""
950 parser.add_argument("cl", metavar="CL", help="The CL to update")
951 parser.add_argument(
952 "hashtags", nargs="+", help="The hashtags to add/remove"
953 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500954
Alex Klein1699fab2022-09-08 08:46:06 -0600955 @staticmethod
956 def __call__(opts):
957 """Implement the action."""
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400958 add, remove = process_add_remove_lists(opts.hashtags)
Alex Klein1699fab2022-09-08 08:46:06 -0600959 helper, cl = GetGerrit(opts, opts.cl)
960 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800961
962
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400963class ActionDelete(_ActionSimpleParallelCLs):
964 """Delete CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500965
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400966 COMMAND = "delete"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500967
Alex Klein1699fab2022-09-08 08:46:06 -0600968 @staticmethod
969 def _process_one(helper, cl, opts):
970 """Use |helper| to process the single |cl|."""
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400971 helper.Delete(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800972
973
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500974class ActionReviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600975 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500976
Alex Klein1699fab2022-09-08 08:46:06 -0600977 COMMAND = "reviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500978
Alex Klein1699fab2022-09-08 08:46:06 -0600979 @staticmethod
980 def _process_one(helper, cl, opts):
981 """Use |helper| to process the single |cl|."""
982 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500983
984
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500985class ActionUnreviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600986 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500987
Alex Klein1699fab2022-09-08 08:46:06 -0600988 COMMAND = "unreviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500989
Alex Klein1699fab2022-09-08 08:46:06 -0600990 @staticmethod
991 def _process_one(helper, cl, opts):
992 """Use |helper| to process the single |cl|."""
993 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500994
995
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500996class ActionIgnore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600997 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500998
Alex Klein1699fab2022-09-08 08:46:06 -0600999 COMMAND = "ignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001000
Alex Klein1699fab2022-09-08 08:46:06 -06001001 @staticmethod
1002 def _process_one(helper, cl, opts):
1003 """Use |helper| to process the single |cl|."""
1004 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -05001005
1006
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001007class ActionUnignore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001008 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001009
Alex Klein1699fab2022-09-08 08:46:06 -06001010 COMMAND = "unignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001011
Alex Klein1699fab2022-09-08 08:46:06 -06001012 @staticmethod
1013 def _process_one(helper, cl, opts):
1014 """Use |helper| to process the single |cl|."""
1015 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -05001016
1017
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001018class ActionCherryPick(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001019 """Cherry-pick CLs to branches."""
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001020
Alex Klein1699fab2022-09-08 08:46:06 -06001021 COMMAND = "cherry-pick"
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001022
Alex Klein1699fab2022-09-08 08:46:06 -06001023 @staticmethod
1024 def init_subparser(parser):
1025 """Add arguments to this action's subparser."""
1026 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
1027 parser.add_argument(
1028 "--rev",
1029 "--revision",
1030 default="current",
1031 help="A specific revision or patchset",
1032 )
1033 parser.add_argument(
1034 "-m",
1035 "--msg",
1036 "--message",
1037 metavar="MESSAGE",
1038 help="Include a message",
1039 )
1040 parser.add_argument(
1041 "--branches",
1042 "--branch",
1043 "--br",
1044 action="split_extend",
1045 default=[],
1046 required=True,
1047 help="The destination branches",
1048 )
1049 parser.add_argument(
1050 "cls", nargs="+", metavar="CL", help="The CLs to cherry-pick"
1051 )
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001052
Alex Klein1699fab2022-09-08 08:46:06 -06001053 @staticmethod
1054 def __call__(opts):
1055 """Implement the action."""
Mike Frysinger16474792023-03-01 01:18:00 -05001056
Alex Klein1699fab2022-09-08 08:46:06 -06001057 # Process branches in parallel, but CLs in serial in case of CL stacks.
1058 def task(branch):
1059 for arg in opts.cls:
1060 helper, cl = GetGerrit(opts, arg)
1061 ret = helper.CherryPick(
1062 cl,
1063 branch,
1064 rev=opts.rev,
1065 msg=opts.msg,
1066 dryrun=opts.dryrun,
1067 notify=opts.notify,
1068 )
1069 logging.debug("Response: %s", ret)
1070 if opts.format is OutputFormat.RAW:
1071 print(ret["_number"])
1072 else:
1073 uri = f'https://{helper.host}/c/{ret["_number"]}'
1074 print(uri_lib.ShortenUri(uri))
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001075
Alex Klein1699fab2022-09-08 08:46:06 -06001076 _run_parallel_tasks(task, *opts.branches)
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001077
1078
Mike Frysinger8037f752020-02-29 20:47:09 -05001079class ActionReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001080 """Review CLs with multiple settings
Mike Frysinger8037f752020-02-29 20:47:09 -05001081
Mike Frysingere5a69832023-06-22 09:34:57 -04001082 The reviewers & cc options can remove people by prepending '~'. Note: If
1083 you want to move someone (reviewer->CC or CC->reviewer), you don't have to
1084 remove them first, you only need to specify the final state.
1085
Alex Klein4507b172023-01-13 11:39:51 -07001086 The label option supports extended/multiple syntax for easy use. The --label
1087 option may be specified multiple times (as settings are merges), and
1088 multiple labels are allowed in a single argument. Each label has the form:
Alex Klein1699fab2022-09-08 08:46:06 -06001089 <long or short name><=+-><value>
Mike Frysinger8037f752020-02-29 20:47:09 -05001090
Alex Klein1699fab2022-09-08 08:46:06 -06001091 Common arguments:
1092 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
1093 'V+1 CQ+2'
1094 'AS=1 V=1'
1095 """
Mike Frysinger8037f752020-02-29 20:47:09 -05001096
Alex Klein1699fab2022-09-08 08:46:06 -06001097 COMMAND = "review"
Mike Frysinger8037f752020-02-29 20:47:09 -05001098
Alex Klein1699fab2022-09-08 08:46:06 -06001099 class _SetLabel(argparse.Action):
1100 """Argparse action for setting labels."""
Mike Frysinger8037f752020-02-29 20:47:09 -05001101
Alex Klein1699fab2022-09-08 08:46:06 -06001102 LABEL_MAP = {
1103 "AS": "Auto-Submit",
1104 "CQ": "Commit-Queue",
1105 "CR": "Code-Review",
1106 "V": "Verified",
1107 }
Mike Frysinger8037f752020-02-29 20:47:09 -05001108
Alex Klein1699fab2022-09-08 08:46:06 -06001109 def __call__(self, parser, namespace, values, option_string=None):
1110 labels = getattr(namespace, self.dest)
1111 for request in values.split():
1112 if "=" in request:
1113 # Handle Verified=1 form.
1114 short, value = request.split("=", 1)
1115 elif "+" in request:
1116 # Handle Verified+1 form.
1117 short, value = request.split("+", 1)
1118 elif "-" in request:
1119 # Handle Verified-1 form.
1120 short, value = request.split("-", 1)
1121 value = "-%s" % (value,)
1122 else:
1123 parser.error(
1124 'Invalid label setting "%s". Must be Commit-Queue=1 or '
1125 "CQ+1 or CR-1." % (request,)
1126 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001127
Alex Klein1699fab2022-09-08 08:46:06 -06001128 # Convert possible short label names like "V" to "Verified".
1129 label = self.LABEL_MAP.get(short)
1130 if not label:
1131 label = short
Mike Frysinger8037f752020-02-29 20:47:09 -05001132
Alex Klein1699fab2022-09-08 08:46:06 -06001133 # We allow existing label requests to be overridden.
1134 labels[label] = value
Mike Frysinger8037f752020-02-29 20:47:09 -05001135
Alex Klein1699fab2022-09-08 08:46:06 -06001136 @classmethod
1137 def init_subparser(cls, parser):
1138 """Add arguments to this action's subparser."""
1139 parser.add_argument(
1140 "-m",
1141 "--msg",
1142 "--message",
1143 metavar="MESSAGE",
1144 help="Include a message",
1145 )
1146 parser.add_argument(
1147 "-l",
1148 "--label",
1149 dest="labels",
1150 action=cls._SetLabel,
1151 default={},
1152 help="Set a label with a value",
1153 )
1154 parser.add_argument(
1155 "--ready",
1156 default=None,
1157 action="store_true",
1158 help="Set CL status to ready-for-review",
1159 )
1160 parser.add_argument(
1161 "--wip",
1162 default=None,
1163 action="store_true",
1164 help="Set CL status to WIP",
1165 )
1166 parser.add_argument(
1167 "--reviewers",
1168 "--re",
1169 action="append",
1170 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",
1176 default=[],
1177 help="People to add/remove in CC",
Alex Klein1699fab2022-09-08 08:46:06 -06001178 )
1179 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger8037f752020-02-29 20:47:09 -05001180
Alex Klein1699fab2022-09-08 08:46:06 -06001181 @staticmethod
1182 def _process_one(helper, cl, opts):
1183 """Use |helper| to process the single |cl|."""
Mike Frysingere5a69832023-06-22 09:34:57 -04001184 add_reviewers, remove_reviewers = process_add_remove_lists(
1185 opts.reviewers, f"^{constants.EMAIL_REGEX}$"
1186 )
1187 add_cc, remove_cc = process_add_remove_lists(
1188 opts.cc, f"^{constants.EMAIL_REGEX}$"
1189 )
1190
1191 # Gerrit allows people to only be in one state: CC or Reviewer. If a
1192 # person is in CC and you want to move them to reviewer, you can't
1193 # remove them from CC and add to reviewer, you have to change their
1194 # state. Help users who do `--cc ~u@c --re u@c` by filtering out all
1195 # the remove requests if there is an add request too. This doesn't
1196 # quite respect all the possible CLI option orders, but it's probably
1197 # good enough for now in practice. For example, mixing of CC & reviewer
1198 # and adds & removes gets complicated.
1199 for add in add_cc:
1200 if add in remove_reviewers:
1201 remove_reviewers.remove(add)
1202 for add in add_reviewers:
1203 if add in remove_cc:
1204 remove_cc.remove(add)
1205
Alex Klein1699fab2022-09-08 08:46:06 -06001206 helper.SetReview(
1207 cl,
1208 msg=opts.msg,
1209 labels=opts.labels,
1210 dryrun=opts.dryrun,
1211 notify=opts.notify,
Mike Frysingere5a69832023-06-22 09:34:57 -04001212 reviewers=add_reviewers,
1213 cc=add_cc,
1214 remove_reviewers=remove_reviewers | remove_cc,
Alex Klein1699fab2022-09-08 08:46:06 -06001215 ready=opts.ready,
1216 wip=opts.wip,
1217 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001218
1219
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001220class ActionAccount(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001221 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001222
Alex Klein1699fab2022-09-08 08:46:06 -06001223 COMMAND = "account"
1224 USE_PAGER = True
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001225
Alex Klein1699fab2022-09-08 08:46:06 -06001226 @staticmethod
1227 def init_subparser(parser):
1228 """Add arguments to this action's subparser."""
1229 parser.add_argument(
1230 "accounts",
1231 nargs="*",
1232 default=["self"],
1233 help="The accounts to query",
1234 )
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001235
Alex Klein1699fab2022-09-08 08:46:06 -06001236 @classmethod
1237 def __call__(cls, opts):
1238 """Implement the action."""
1239 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001240
Alex Klein1699fab2022-09-08 08:46:06 -06001241 def print_one(header, data):
1242 print(f"### {header}")
1243 compact = opts.format is OutputFormat.JSON
1244 print(pformat.json(data, compact=compact).rstrip())
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001245
Alex Klein1699fab2022-09-08 08:46:06 -06001246 def task(arg):
1247 detail = gob_util.FetchUrlJson(
1248 helper.host, f"accounts/{arg}/detail"
1249 )
1250 if not detail:
1251 print(f"{arg}: account not found")
1252 else:
1253 print_one("detail", detail)
1254 for field in (
Mike Frysinger03c75072023-03-10 17:25:21 -05001255 "external.ids",
Alex Klein1699fab2022-09-08 08:46:06 -06001256 "groups",
1257 "capabilities",
1258 "preferences",
1259 "sshkeys",
1260 "gpgkeys",
1261 ):
1262 data = gob_util.FetchUrlJson(
1263 helper.host, f"accounts/{arg}/{field}"
1264 )
1265 print_one(field, data)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001266
Alex Klein1699fab2022-09-08 08:46:06 -06001267 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -08001268
1269
Mike Frysinger2295d792021-03-08 15:55:23 -05001270class ActionConfig(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001271 """Manage the gerrit tool's own config file
Mike Frysinger2295d792021-03-08 15:55:23 -05001272
Alex Klein1699fab2022-09-08 08:46:06 -06001273 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
1274 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
Mike Frysinger2295d792021-03-08 15:55:23 -05001275
Alex Klein1699fab2022-09-08 08:46:06 -06001276 # Set up subcommand aliases.
1277 [alias]
1278 common-search = search 'is:open project:something/i/care/about'
1279 """
Mike Frysinger2295d792021-03-08 15:55:23 -05001280
Alex Klein1699fab2022-09-08 08:46:06 -06001281 COMMAND = "config"
Mike Frysinger2295d792021-03-08 15:55:23 -05001282
Alex Klein1699fab2022-09-08 08:46:06 -06001283 @staticmethod
1284 def __call__(opts):
1285 """Implement the action."""
Alex Klein4507b172023-01-13 11:39:51 -07001286 # For now, this is a place holder for raising visibility for the config
1287 # file and its associated help text documentation.
Alex Klein1699fab2022-09-08 08:46:06 -06001288 opts.parser.parse_args(["config", "--help"])
Mike Frysinger2295d792021-03-08 15:55:23 -05001289
1290
Mike Frysingere5450602021-03-08 15:34:17 -05001291class ActionHelp(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001292 """An alias to --help for CLI symmetry"""
Mike Frysingere5450602021-03-08 15:34:17 -05001293
Alex Klein1699fab2022-09-08 08:46:06 -06001294 COMMAND = "help"
1295 USE_PAGER = True
Mike Frysingere5450602021-03-08 15:34:17 -05001296
Alex Klein1699fab2022-09-08 08:46:06 -06001297 @staticmethod
1298 def init_subparser(parser):
1299 """Add arguments to this action's subparser."""
1300 parser.add_argument(
1301 "command", nargs="?", help="The command to display."
1302 )
Mike Frysingere5450602021-03-08 15:34:17 -05001303
Alex Klein1699fab2022-09-08 08:46:06 -06001304 @staticmethod
1305 def __call__(opts):
1306 """Implement the action."""
1307 # Show global help.
1308 if not opts.command:
1309 opts.parser.print_help()
1310 return
Mike Frysingere5450602021-03-08 15:34:17 -05001311
Alex Klein1699fab2022-09-08 08:46:06 -06001312 opts.parser.parse_args([opts.command, "--help"])
Mike Frysingere5450602021-03-08 15:34:17 -05001313
1314
Mike Frysinger484e2f82020-03-20 01:41:10 -04001315class ActionHelpAll(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001316 """Show all actions help output at once."""
Mike Frysinger484e2f82020-03-20 01:41:10 -04001317
Alex Klein1699fab2022-09-08 08:46:06 -06001318 COMMAND = "help-all"
1319 USE_PAGER = True
Mike Frysinger484e2f82020-03-20 01:41:10 -04001320
Alex Klein1699fab2022-09-08 08:46:06 -06001321 @staticmethod
1322 def __call__(opts):
1323 """Implement the action."""
1324 first = True
1325 for action in _GetActions():
1326 if first:
1327 first = False
1328 else:
1329 print("\n\n")
Mike Frysinger484e2f82020-03-20 01:41:10 -04001330
Alex Klein1699fab2022-09-08 08:46:06 -06001331 try:
1332 opts.parser.parse_args([action, "--help"])
1333 except SystemExit:
1334 pass
Mike Frysinger484e2f82020-03-20 01:41:10 -04001335
1336
Mike Frysinger65fc8632020-02-06 18:11:12 -05001337@memoize.Memoize
1338def _GetActions():
Alex Klein1699fab2022-09-08 08:46:06 -06001339 """Get all the possible actions we support.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001340
Alex Klein1699fab2022-09-08 08:46:06 -06001341 Returns:
Trent Apted66736d82023-05-25 10:38:28 +10001342 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1343 function that implements that command (e.g. UserActFoo).
Alex Klein1699fab2022-09-08 08:46:06 -06001344 """
1345 VALID_NAME = re.compile(r"^[a-z][a-z-]*[a-z]$")
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001346
Alex Klein1699fab2022-09-08 08:46:06 -06001347 actions = {}
1348 for cls in globals().values():
1349 if (
1350 not inspect.isclass(cls)
1351 or not issubclass(cls, UserAction)
1352 or not getattr(cls, "COMMAND", None)
1353 ):
1354 continue
Mike Frysinger65fc8632020-02-06 18:11:12 -05001355
Alex Klein1699fab2022-09-08 08:46:06 -06001356 # Sanity check names for devs adding new commands. Should be quick.
1357 cmd = cls.COMMAND
1358 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1359 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001360
Alex Klein1699fab2022-09-08 08:46:06 -06001361 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001362
Alex Klein1699fab2022-09-08 08:46:06 -06001363 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001364
1365
Harry Cutts26076b32019-02-26 15:01:29 -08001366def _GetActionUsages():
Alex Klein1699fab2022-09-08 08:46:06 -06001367 """Formats a one-line usage and doc message for each action."""
1368 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001369
Alex Klein1699fab2022-09-08 08:46:06 -06001370 cmds = list(actions.keys())
1371 functions = list(actions.values())
1372 usages = [getattr(x, "usage", "") for x in functions]
1373 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001374
Alex Klein1699fab2022-09-08 08:46:06 -06001375 cmd_indent = len(max(cmds, key=len))
1376 usage_indent = len(max(usages, key=len))
1377 return "\n".join(
1378 " %-*s %-*s : %s" % (cmd_indent, cmd, usage_indent, usage, doc)
1379 for cmd, usage, doc in zip(cmds, usages, docs)
1380 )
Harry Cutts26076b32019-02-26 15:01:29 -08001381
1382
Mike Frysinger2295d792021-03-08 15:55:23 -05001383def _AddCommonOptions(parser, subparser):
Alex Klein1699fab2022-09-08 08:46:06 -06001384 """Add options that should work before & after the subcommand.
Mike Frysinger2295d792021-03-08 15:55:23 -05001385
Alex Klein1699fab2022-09-08 08:46:06 -06001386 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1387 """
1388 parser.add_common_argument_to_group(
1389 subparser,
1390 "--ne",
1391 "--no-emails",
1392 dest="notify",
1393 default="ALL",
1394 action="store_const",
1395 const="NONE",
1396 help="Do not send e-mail notifications",
1397 )
1398 parser.add_common_argument_to_group(
1399 subparser,
1400 "-n",
1401 "--dry-run",
1402 dest="dryrun",
1403 default=False,
1404 action="store_true",
1405 help="Show what would be done, but do not make changes",
1406 )
Mike Frysinger2295d792021-03-08 15:55:23 -05001407
1408
1409def GetBaseParser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001410 """Returns the common parser (i.e. no subparsers added)."""
1411 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001412There is no support for doing line-by-line code review via the command line.
1413This helps you manage various bits and CL status.
1414
Mike Frysingera1db2c42014-06-15 00:42:48 -07001415For general Gerrit documentation, see:
1416 https://gerrit-review.googlesource.com/Documentation/
1417The Searching Changes page covers the search query syntax:
1418 https://gerrit-review.googlesource.com/Documentation/user-search.html
1419
Mike Frysinger13f23a42013-05-13 17:32:01 -04001420Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001421 $ gerrit todo # List all the CLs that await your review.
1422 $ gerrit mine # List all of your open CLs.
1423 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1424 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1425 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001426 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
142728123.
1428 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1429CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001430Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001431 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1432with Commit-Queue=1.
1433 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1434CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001435 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001436
Harry Cutts26076b32019-02-26 15:01:29 -08001437Actions:
1438"""
Alex Klein1699fab2022-09-08 08:46:06 -06001439 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001440
Alex Klein1699fab2022-09-08 08:46:06 -06001441 site_params = config_lib.GetSiteParams()
1442 parser = commandline.ArgumentParser(
1443 description=description,
1444 default_log_level="notice",
1445 epilog="For subcommand help, use `gerrit help <command>`.",
1446 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001447
Alex Klein1699fab2022-09-08 08:46:06 -06001448 group = parser.add_argument_group("Server options")
1449 group.add_argument(
1450 "-i",
1451 "--internal",
1452 dest="gob",
1453 action="store_const",
1454 default=site_params.EXTERNAL_GOB_INSTANCE,
1455 const=site_params.INTERNAL_GOB_INSTANCE,
1456 help="Query internal Chrome Gerrit instance",
1457 )
1458 group.add_argument(
1459 "-g",
1460 "--gob",
1461 default=site_params.EXTERNAL_GOB_INSTANCE,
Trent Apted66736d82023-05-25 10:38:28 +10001462 help="Gerrit (on borg) instance to query (default: %(default)s)",
Alex Klein1699fab2022-09-08 08:46:06 -06001463 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001464
Alex Klein1699fab2022-09-08 08:46:06 -06001465 group = parser.add_argument_group("CL options")
1466 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001467
Alex Klein1699fab2022-09-08 08:46:06 -06001468 group = parser.add_mutually_exclusive_group()
1469 parser.set_defaults(format=OutputFormat.AUTO)
1470 group.add_argument(
1471 "--format",
1472 action="enum",
1473 enum=OutputFormat,
1474 help="Output format to use.",
1475 )
1476 group.add_argument(
1477 "--raw",
1478 action="store_const",
1479 dest="format",
1480 const=OutputFormat.RAW,
1481 help="Alias for --format=raw.",
1482 )
1483 group.add_argument(
1484 "--json",
1485 action="store_const",
1486 dest="format",
1487 const=OutputFormat.JSON,
1488 help="Alias for --format=json.",
1489 )
Jack Rosenthal95aac172022-06-30 15:35:07 -06001490
Alex Klein1699fab2022-09-08 08:46:06 -06001491 group = parser.add_mutually_exclusive_group()
1492 group.add_argument(
1493 "--pager",
1494 action="store_true",
1495 default=sys.stdout.isatty(),
1496 help="Enable pager.",
1497 )
1498 group.add_argument(
1499 "--no-pager", action="store_false", dest="pager", help="Disable pager."
1500 )
1501 return parser
Mike Frysinger2295d792021-03-08 15:55:23 -05001502
1503
Alex Klein1699fab2022-09-08 08:46:06 -06001504def GetParser(
1505 parser: commandline.ArgumentParser = None,
Mike Frysinger16474792023-03-01 01:18:00 -05001506) -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001507 """Returns the full parser to use for this module."""
1508 if parser is None:
1509 parser = GetBaseParser()
Mike Frysinger2295d792021-03-08 15:55:23 -05001510
Alex Klein1699fab2022-09-08 08:46:06 -06001511 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001512
Alex Klein1699fab2022-09-08 08:46:06 -06001513 # Subparsers are required by default under Python 2. Python 3 changed to
1514 # not required, but didn't include a required option until 3.7. Setting
1515 # the required member works in all versions (and setting dest name).
1516 subparsers = parser.add_subparsers(dest="action")
1517 subparsers.required = True
1518 for cmd, cls in actions.items():
1519 # Format the full docstring by removing the file level indentation.
1520 description = re.sub(r"^ ", "", cls.__doc__, flags=re.M)
1521 subparser = subparsers.add_parser(cmd, description=description)
1522 _AddCommonOptions(parser, subparser)
1523 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001524
Alex Klein1699fab2022-09-08 08:46:06 -06001525 return parser
Mike Frysinger108eda22018-06-06 18:45:12 -04001526
1527
Jack Rosenthal95aac172022-06-30 15:35:07 -06001528def start_pager():
Alex Klein1699fab2022-09-08 08:46:06 -06001529 """Re-spawn ourselves attached to a pager."""
1530 pager = os.environ.get("PAGER", "less")
1531 os.environ.setdefault("LESS", "FRX")
Jack Rosenthal95aac172022-06-30 15:35:07 -06001532 with subprocess.Popen(
Alex Klein1699fab2022-09-08 08:46:06 -06001533 # sys.argv can have some edge cases: we may not necessarily use
1534 # sys.executable if the script is executed as "python path/to/script".
1535 # If we upgrade to Python 3.10+, this should be changed to sys.orig_argv
1536 # for full accuracy.
1537 sys.argv,
1538 stdout=subprocess.PIPE,
1539 stderr=subprocess.STDOUT,
1540 env={"GERRIT_RESPAWN_FOR_PAGER": "1", **os.environ},
1541 ) as gerrit_proc:
1542 with subprocess.Popen(
1543 pager,
1544 shell=True,
1545 stdin=gerrit_proc.stdout,
1546 ) as pager_proc:
1547 # Send SIGINT to just the gerrit process, not the pager too.
1548 def _sighandler(signum, _frame):
1549 gerrit_proc.send_signal(signum)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001550
Alex Klein1699fab2022-09-08 08:46:06 -06001551 signal.signal(signal.SIGINT, _sighandler)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001552
Alex Klein1699fab2022-09-08 08:46:06 -06001553 pager_proc.communicate()
1554 # If the pager exits, and the gerrit process is still running, we
1555 # must terminate it.
1556 if gerrit_proc.poll() is None:
1557 gerrit_proc.terminate()
1558 sys.exit(gerrit_proc.wait())
Jack Rosenthal95aac172022-06-30 15:35:07 -06001559
1560
Mike Frysinger108eda22018-06-06 18:45:12 -04001561def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -06001562 base_parser = GetBaseParser()
1563 opts, subargs = base_parser.parse_known_args(argv)
Mike Frysinger2295d792021-03-08 15:55:23 -05001564
Alex Klein1699fab2022-09-08 08:46:06 -06001565 config = Config()
1566 if subargs:
Alex Klein4507b172023-01-13 11:39:51 -07001567 # If the action is an alias to an expanded value, we need to mutate the
1568 # argv and reparse things.
Alex Klein1699fab2022-09-08 08:46:06 -06001569 action = config.expand_alias(subargs[0])
1570 if action != subargs[0]:
1571 pos = argv.index(subargs[0])
1572 argv = argv[:pos] + action + argv[pos + 1 :]
Mike Frysinger2295d792021-03-08 15:55:23 -05001573
Alex Klein1699fab2022-09-08 08:46:06 -06001574 parser = GetParser(parser=base_parser)
1575 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001576
Alex Klein1699fab2022-09-08 08:46:06 -06001577 # If we're running as a re-spawn for the pager, from this point on
1578 # we'll pretend we're attached to a TTY. This will give us colored
1579 # output when requested.
1580 if os.environ.pop("GERRIT_RESPAWN_FOR_PAGER", None) is not None:
1581 opts.pager = False
1582 sys.stdout.isatty = lambda: True
Jack Rosenthal95aac172022-06-30 15:35:07 -06001583
Alex Klein1699fab2022-09-08 08:46:06 -06001584 # In case the action wants to throw a parser error.
1585 opts.parser = parser
Mike Frysinger484e2f82020-03-20 01:41:10 -04001586
Alex Klein1699fab2022-09-08 08:46:06 -06001587 # A cache of gerrit helpers we'll load on demand.
1588 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001589
Alex Klein1699fab2022-09-08 08:46:06 -06001590 if opts.format is OutputFormat.AUTO:
1591 if sys.stdout.isatty():
1592 opts.format = OutputFormat.PRETTY
1593 else:
1594 opts.format = OutputFormat.RAW
Jack Rosenthale3a92672022-06-29 14:54:48 -06001595
Alex Klein1699fab2022-09-08 08:46:06 -06001596 opts.Freeze()
Mike Frysinger88f27292014-06-17 09:40:45 -07001597
Alex Klein1699fab2022-09-08 08:46:06 -06001598 # pylint: disable=global-statement
1599 global COLOR
1600 COLOR = terminal.Color(enabled=opts.color)
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001601
Alex Klein1699fab2022-09-08 08:46:06 -06001602 # Now look up the requested user action and run it.
1603 actions = _GetActions()
1604 action_class = actions[opts.action]
1605 if action_class.USE_PAGER and opts.pager:
1606 start_pager()
1607 obj = action_class()
1608 try:
1609 obj(opts)
1610 except (
1611 cros_build_lib.RunCommandError,
1612 gerrit.GerritException,
1613 gob_util.GOBError,
1614 ) as e:
1615 cros_build_lib.Die(e)