blob: 82be5e0912a43a7988a9de4873f3d9b34d2bc261 [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
Alex Klein4507b172023-01-13 11:39:51 -07001082 The label option supports extended/multiple syntax for easy use. The --label
1083 option may be specified multiple times (as settings are merges), and
1084 multiple labels are allowed in a single argument. Each label has the form:
Alex Klein1699fab2022-09-08 08:46:06 -06001085 <long or short name><=+-><value>
Mike Frysinger8037f752020-02-29 20:47:09 -05001086
Alex Klein1699fab2022-09-08 08:46:06 -06001087 Common arguments:
1088 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
1089 'V+1 CQ+2'
1090 'AS=1 V=1'
1091 """
Mike Frysinger8037f752020-02-29 20:47:09 -05001092
Alex Klein1699fab2022-09-08 08:46:06 -06001093 COMMAND = "review"
Mike Frysinger8037f752020-02-29 20:47:09 -05001094
Alex Klein1699fab2022-09-08 08:46:06 -06001095 class _SetLabel(argparse.Action):
1096 """Argparse action for setting labels."""
Mike Frysinger8037f752020-02-29 20:47:09 -05001097
Alex Klein1699fab2022-09-08 08:46:06 -06001098 LABEL_MAP = {
1099 "AS": "Auto-Submit",
1100 "CQ": "Commit-Queue",
1101 "CR": "Code-Review",
1102 "V": "Verified",
1103 }
Mike Frysinger8037f752020-02-29 20:47:09 -05001104
Alex Klein1699fab2022-09-08 08:46:06 -06001105 def __call__(self, parser, namespace, values, option_string=None):
1106 labels = getattr(namespace, self.dest)
1107 for request in values.split():
1108 if "=" in request:
1109 # Handle Verified=1 form.
1110 short, value = request.split("=", 1)
1111 elif "+" in request:
1112 # Handle Verified+1 form.
1113 short, value = request.split("+", 1)
1114 elif "-" in request:
1115 # Handle Verified-1 form.
1116 short, value = request.split("-", 1)
1117 value = "-%s" % (value,)
1118 else:
1119 parser.error(
1120 'Invalid label setting "%s". Must be Commit-Queue=1 or '
1121 "CQ+1 or CR-1." % (request,)
1122 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001123
Alex Klein1699fab2022-09-08 08:46:06 -06001124 # Convert possible short label names like "V" to "Verified".
1125 label = self.LABEL_MAP.get(short)
1126 if not label:
1127 label = short
Mike Frysinger8037f752020-02-29 20:47:09 -05001128
Alex Klein1699fab2022-09-08 08:46:06 -06001129 # We allow existing label requests to be overridden.
1130 labels[label] = value
Mike Frysinger8037f752020-02-29 20:47:09 -05001131
Alex Klein1699fab2022-09-08 08:46:06 -06001132 @classmethod
1133 def init_subparser(cls, parser):
1134 """Add arguments to this action's subparser."""
1135 parser.add_argument(
1136 "-m",
1137 "--msg",
1138 "--message",
1139 metavar="MESSAGE",
1140 help="Include a message",
1141 )
1142 parser.add_argument(
1143 "-l",
1144 "--label",
1145 dest="labels",
1146 action=cls._SetLabel,
1147 default={},
1148 help="Set a label with a value",
1149 )
1150 parser.add_argument(
1151 "--ready",
1152 default=None,
1153 action="store_true",
1154 help="Set CL status to ready-for-review",
1155 )
1156 parser.add_argument(
1157 "--wip",
1158 default=None,
1159 action="store_true",
1160 help="Set CL status to WIP",
1161 )
1162 parser.add_argument(
1163 "--reviewers",
1164 "--re",
1165 action="append",
1166 default=[],
1167 help="Add reviewers",
1168 )
1169 parser.add_argument(
1170 "--cc", action="append", default=[], help="Add people to CC"
1171 )
1172 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger8037f752020-02-29 20:47:09 -05001173
Alex Klein1699fab2022-09-08 08:46:06 -06001174 @staticmethod
1175 def _process_one(helper, cl, opts):
1176 """Use |helper| to process the single |cl|."""
1177 helper.SetReview(
1178 cl,
1179 msg=opts.msg,
1180 labels=opts.labels,
1181 dryrun=opts.dryrun,
1182 notify=opts.notify,
1183 reviewers=opts.reviewers,
1184 cc=opts.cc,
1185 ready=opts.ready,
1186 wip=opts.wip,
1187 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001188
1189
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001190class ActionAccount(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001191 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001192
Alex Klein1699fab2022-09-08 08:46:06 -06001193 COMMAND = "account"
1194 USE_PAGER = True
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001195
Alex Klein1699fab2022-09-08 08:46:06 -06001196 @staticmethod
1197 def init_subparser(parser):
1198 """Add arguments to this action's subparser."""
1199 parser.add_argument(
1200 "accounts",
1201 nargs="*",
1202 default=["self"],
1203 help="The accounts to query",
1204 )
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001205
Alex Klein1699fab2022-09-08 08:46:06 -06001206 @classmethod
1207 def __call__(cls, opts):
1208 """Implement the action."""
1209 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001210
Alex Klein1699fab2022-09-08 08:46:06 -06001211 def print_one(header, data):
1212 print(f"### {header}")
1213 compact = opts.format is OutputFormat.JSON
1214 print(pformat.json(data, compact=compact).rstrip())
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001215
Alex Klein1699fab2022-09-08 08:46:06 -06001216 def task(arg):
1217 detail = gob_util.FetchUrlJson(
1218 helper.host, f"accounts/{arg}/detail"
1219 )
1220 if not detail:
1221 print(f"{arg}: account not found")
1222 else:
1223 print_one("detail", detail)
1224 for field in (
Mike Frysinger03c75072023-03-10 17:25:21 -05001225 "external.ids",
Alex Klein1699fab2022-09-08 08:46:06 -06001226 "groups",
1227 "capabilities",
1228 "preferences",
1229 "sshkeys",
1230 "gpgkeys",
1231 ):
1232 data = gob_util.FetchUrlJson(
1233 helper.host, f"accounts/{arg}/{field}"
1234 )
1235 print_one(field, data)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001236
Alex Klein1699fab2022-09-08 08:46:06 -06001237 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -08001238
1239
Mike Frysinger2295d792021-03-08 15:55:23 -05001240class ActionConfig(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001241 """Manage the gerrit tool's own config file
Mike Frysinger2295d792021-03-08 15:55:23 -05001242
Alex Klein1699fab2022-09-08 08:46:06 -06001243 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
1244 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
Mike Frysinger2295d792021-03-08 15:55:23 -05001245
Alex Klein1699fab2022-09-08 08:46:06 -06001246 # Set up subcommand aliases.
1247 [alias]
1248 common-search = search 'is:open project:something/i/care/about'
1249 """
Mike Frysinger2295d792021-03-08 15:55:23 -05001250
Alex Klein1699fab2022-09-08 08:46:06 -06001251 COMMAND = "config"
Mike Frysinger2295d792021-03-08 15:55:23 -05001252
Alex Klein1699fab2022-09-08 08:46:06 -06001253 @staticmethod
1254 def __call__(opts):
1255 """Implement the action."""
Alex Klein4507b172023-01-13 11:39:51 -07001256 # For now, this is a place holder for raising visibility for the config
1257 # file and its associated help text documentation.
Alex Klein1699fab2022-09-08 08:46:06 -06001258 opts.parser.parse_args(["config", "--help"])
Mike Frysinger2295d792021-03-08 15:55:23 -05001259
1260
Mike Frysingere5450602021-03-08 15:34:17 -05001261class ActionHelp(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001262 """An alias to --help for CLI symmetry"""
Mike Frysingere5450602021-03-08 15:34:17 -05001263
Alex Klein1699fab2022-09-08 08:46:06 -06001264 COMMAND = "help"
1265 USE_PAGER = True
Mike Frysingere5450602021-03-08 15:34:17 -05001266
Alex Klein1699fab2022-09-08 08:46:06 -06001267 @staticmethod
1268 def init_subparser(parser):
1269 """Add arguments to this action's subparser."""
1270 parser.add_argument(
1271 "command", nargs="?", help="The command to display."
1272 )
Mike Frysingere5450602021-03-08 15:34:17 -05001273
Alex Klein1699fab2022-09-08 08:46:06 -06001274 @staticmethod
1275 def __call__(opts):
1276 """Implement the action."""
1277 # Show global help.
1278 if not opts.command:
1279 opts.parser.print_help()
1280 return
Mike Frysingere5450602021-03-08 15:34:17 -05001281
Alex Klein1699fab2022-09-08 08:46:06 -06001282 opts.parser.parse_args([opts.command, "--help"])
Mike Frysingere5450602021-03-08 15:34:17 -05001283
1284
Mike Frysinger484e2f82020-03-20 01:41:10 -04001285class ActionHelpAll(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001286 """Show all actions help output at once."""
Mike Frysinger484e2f82020-03-20 01:41:10 -04001287
Alex Klein1699fab2022-09-08 08:46:06 -06001288 COMMAND = "help-all"
1289 USE_PAGER = True
Mike Frysinger484e2f82020-03-20 01:41:10 -04001290
Alex Klein1699fab2022-09-08 08:46:06 -06001291 @staticmethod
1292 def __call__(opts):
1293 """Implement the action."""
1294 first = True
1295 for action in _GetActions():
1296 if first:
1297 first = False
1298 else:
1299 print("\n\n")
Mike Frysinger484e2f82020-03-20 01:41:10 -04001300
Alex Klein1699fab2022-09-08 08:46:06 -06001301 try:
1302 opts.parser.parse_args([action, "--help"])
1303 except SystemExit:
1304 pass
Mike Frysinger484e2f82020-03-20 01:41:10 -04001305
1306
Mike Frysinger65fc8632020-02-06 18:11:12 -05001307@memoize.Memoize
1308def _GetActions():
Alex Klein1699fab2022-09-08 08:46:06 -06001309 """Get all the possible actions we support.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001310
Alex Klein1699fab2022-09-08 08:46:06 -06001311 Returns:
Trent Apted66736d82023-05-25 10:38:28 +10001312 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1313 function that implements that command (e.g. UserActFoo).
Alex Klein1699fab2022-09-08 08:46:06 -06001314 """
1315 VALID_NAME = re.compile(r"^[a-z][a-z-]*[a-z]$")
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001316
Alex Klein1699fab2022-09-08 08:46:06 -06001317 actions = {}
1318 for cls in globals().values():
1319 if (
1320 not inspect.isclass(cls)
1321 or not issubclass(cls, UserAction)
1322 or not getattr(cls, "COMMAND", None)
1323 ):
1324 continue
Mike Frysinger65fc8632020-02-06 18:11:12 -05001325
Alex Klein1699fab2022-09-08 08:46:06 -06001326 # Sanity check names for devs adding new commands. Should be quick.
1327 cmd = cls.COMMAND
1328 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1329 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001330
Alex Klein1699fab2022-09-08 08:46:06 -06001331 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001332
Alex Klein1699fab2022-09-08 08:46:06 -06001333 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001334
1335
Harry Cutts26076b32019-02-26 15:01:29 -08001336def _GetActionUsages():
Alex Klein1699fab2022-09-08 08:46:06 -06001337 """Formats a one-line usage and doc message for each action."""
1338 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001339
Alex Klein1699fab2022-09-08 08:46:06 -06001340 cmds = list(actions.keys())
1341 functions = list(actions.values())
1342 usages = [getattr(x, "usage", "") for x in functions]
1343 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001344
Alex Klein1699fab2022-09-08 08:46:06 -06001345 cmd_indent = len(max(cmds, key=len))
1346 usage_indent = len(max(usages, key=len))
1347 return "\n".join(
1348 " %-*s %-*s : %s" % (cmd_indent, cmd, usage_indent, usage, doc)
1349 for cmd, usage, doc in zip(cmds, usages, docs)
1350 )
Harry Cutts26076b32019-02-26 15:01:29 -08001351
1352
Mike Frysinger2295d792021-03-08 15:55:23 -05001353def _AddCommonOptions(parser, subparser):
Alex Klein1699fab2022-09-08 08:46:06 -06001354 """Add options that should work before & after the subcommand.
Mike Frysinger2295d792021-03-08 15:55:23 -05001355
Alex Klein1699fab2022-09-08 08:46:06 -06001356 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1357 """
1358 parser.add_common_argument_to_group(
1359 subparser,
1360 "--ne",
1361 "--no-emails",
1362 dest="notify",
1363 default="ALL",
1364 action="store_const",
1365 const="NONE",
1366 help="Do not send e-mail notifications",
1367 )
1368 parser.add_common_argument_to_group(
1369 subparser,
1370 "-n",
1371 "--dry-run",
1372 dest="dryrun",
1373 default=False,
1374 action="store_true",
1375 help="Show what would be done, but do not make changes",
1376 )
Mike Frysinger2295d792021-03-08 15:55:23 -05001377
1378
1379def GetBaseParser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001380 """Returns the common parser (i.e. no subparsers added)."""
1381 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001382There is no support for doing line-by-line code review via the command line.
1383This helps you manage various bits and CL status.
1384
Mike Frysingera1db2c42014-06-15 00:42:48 -07001385For general Gerrit documentation, see:
1386 https://gerrit-review.googlesource.com/Documentation/
1387The Searching Changes page covers the search query syntax:
1388 https://gerrit-review.googlesource.com/Documentation/user-search.html
1389
Mike Frysinger13f23a42013-05-13 17:32:01 -04001390Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001391 $ gerrit todo # List all the CLs that await your review.
1392 $ gerrit mine # List all of your open CLs.
1393 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1394 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1395 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001396 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
139728123.
1398 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1399CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001400Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001401 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1402with Commit-Queue=1.
1403 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1404CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001405 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001406
Harry Cutts26076b32019-02-26 15:01:29 -08001407Actions:
1408"""
Alex Klein1699fab2022-09-08 08:46:06 -06001409 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001410
Alex Klein1699fab2022-09-08 08:46:06 -06001411 site_params = config_lib.GetSiteParams()
1412 parser = commandline.ArgumentParser(
1413 description=description,
1414 default_log_level="notice",
1415 epilog="For subcommand help, use `gerrit help <command>`.",
1416 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001417
Alex Klein1699fab2022-09-08 08:46:06 -06001418 group = parser.add_argument_group("Server options")
1419 group.add_argument(
1420 "-i",
1421 "--internal",
1422 dest="gob",
1423 action="store_const",
1424 default=site_params.EXTERNAL_GOB_INSTANCE,
1425 const=site_params.INTERNAL_GOB_INSTANCE,
1426 help="Query internal Chrome Gerrit instance",
1427 )
1428 group.add_argument(
1429 "-g",
1430 "--gob",
1431 default=site_params.EXTERNAL_GOB_INSTANCE,
Trent Apted66736d82023-05-25 10:38:28 +10001432 help="Gerrit (on borg) instance to query (default: %(default)s)",
Alex Klein1699fab2022-09-08 08:46:06 -06001433 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001434
Alex Klein1699fab2022-09-08 08:46:06 -06001435 group = parser.add_argument_group("CL options")
1436 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001437
Alex Klein1699fab2022-09-08 08:46:06 -06001438 group = parser.add_mutually_exclusive_group()
1439 parser.set_defaults(format=OutputFormat.AUTO)
1440 group.add_argument(
1441 "--format",
1442 action="enum",
1443 enum=OutputFormat,
1444 help="Output format to use.",
1445 )
1446 group.add_argument(
1447 "--raw",
1448 action="store_const",
1449 dest="format",
1450 const=OutputFormat.RAW,
1451 help="Alias for --format=raw.",
1452 )
1453 group.add_argument(
1454 "--json",
1455 action="store_const",
1456 dest="format",
1457 const=OutputFormat.JSON,
1458 help="Alias for --format=json.",
1459 )
Jack Rosenthal95aac172022-06-30 15:35:07 -06001460
Alex Klein1699fab2022-09-08 08:46:06 -06001461 group = parser.add_mutually_exclusive_group()
1462 group.add_argument(
1463 "--pager",
1464 action="store_true",
1465 default=sys.stdout.isatty(),
1466 help="Enable pager.",
1467 )
1468 group.add_argument(
1469 "--no-pager", action="store_false", dest="pager", help="Disable pager."
1470 )
1471 return parser
Mike Frysinger2295d792021-03-08 15:55:23 -05001472
1473
Alex Klein1699fab2022-09-08 08:46:06 -06001474def GetParser(
1475 parser: commandline.ArgumentParser = None,
Mike Frysinger16474792023-03-01 01:18:00 -05001476) -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001477 """Returns the full parser to use for this module."""
1478 if parser is None:
1479 parser = GetBaseParser()
Mike Frysinger2295d792021-03-08 15:55:23 -05001480
Alex Klein1699fab2022-09-08 08:46:06 -06001481 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001482
Alex Klein1699fab2022-09-08 08:46:06 -06001483 # Subparsers are required by default under Python 2. Python 3 changed to
1484 # not required, but didn't include a required option until 3.7. Setting
1485 # the required member works in all versions (and setting dest name).
1486 subparsers = parser.add_subparsers(dest="action")
1487 subparsers.required = True
1488 for cmd, cls in actions.items():
1489 # Format the full docstring by removing the file level indentation.
1490 description = re.sub(r"^ ", "", cls.__doc__, flags=re.M)
1491 subparser = subparsers.add_parser(cmd, description=description)
1492 _AddCommonOptions(parser, subparser)
1493 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001494
Alex Klein1699fab2022-09-08 08:46:06 -06001495 return parser
Mike Frysinger108eda22018-06-06 18:45:12 -04001496
1497
Jack Rosenthal95aac172022-06-30 15:35:07 -06001498def start_pager():
Alex Klein1699fab2022-09-08 08:46:06 -06001499 """Re-spawn ourselves attached to a pager."""
1500 pager = os.environ.get("PAGER", "less")
1501 os.environ.setdefault("LESS", "FRX")
Jack Rosenthal95aac172022-06-30 15:35:07 -06001502 with subprocess.Popen(
Alex Klein1699fab2022-09-08 08:46:06 -06001503 # sys.argv can have some edge cases: we may not necessarily use
1504 # sys.executable if the script is executed as "python path/to/script".
1505 # If we upgrade to Python 3.10+, this should be changed to sys.orig_argv
1506 # for full accuracy.
1507 sys.argv,
1508 stdout=subprocess.PIPE,
1509 stderr=subprocess.STDOUT,
1510 env={"GERRIT_RESPAWN_FOR_PAGER": "1", **os.environ},
1511 ) as gerrit_proc:
1512 with subprocess.Popen(
1513 pager,
1514 shell=True,
1515 stdin=gerrit_proc.stdout,
1516 ) as pager_proc:
1517 # Send SIGINT to just the gerrit process, not the pager too.
1518 def _sighandler(signum, _frame):
1519 gerrit_proc.send_signal(signum)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001520
Alex Klein1699fab2022-09-08 08:46:06 -06001521 signal.signal(signal.SIGINT, _sighandler)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001522
Alex Klein1699fab2022-09-08 08:46:06 -06001523 pager_proc.communicate()
1524 # If the pager exits, and the gerrit process is still running, we
1525 # must terminate it.
1526 if gerrit_proc.poll() is None:
1527 gerrit_proc.terminate()
1528 sys.exit(gerrit_proc.wait())
Jack Rosenthal95aac172022-06-30 15:35:07 -06001529
1530
Mike Frysinger108eda22018-06-06 18:45:12 -04001531def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -06001532 base_parser = GetBaseParser()
1533 opts, subargs = base_parser.parse_known_args(argv)
Mike Frysinger2295d792021-03-08 15:55:23 -05001534
Alex Klein1699fab2022-09-08 08:46:06 -06001535 config = Config()
1536 if subargs:
Alex Klein4507b172023-01-13 11:39:51 -07001537 # If the action is an alias to an expanded value, we need to mutate the
1538 # argv and reparse things.
Alex Klein1699fab2022-09-08 08:46:06 -06001539 action = config.expand_alias(subargs[0])
1540 if action != subargs[0]:
1541 pos = argv.index(subargs[0])
1542 argv = argv[:pos] + action + argv[pos + 1 :]
Mike Frysinger2295d792021-03-08 15:55:23 -05001543
Alex Klein1699fab2022-09-08 08:46:06 -06001544 parser = GetParser(parser=base_parser)
1545 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001546
Alex Klein1699fab2022-09-08 08:46:06 -06001547 # If we're running as a re-spawn for the pager, from this point on
1548 # we'll pretend we're attached to a TTY. This will give us colored
1549 # output when requested.
1550 if os.environ.pop("GERRIT_RESPAWN_FOR_PAGER", None) is not None:
1551 opts.pager = False
1552 sys.stdout.isatty = lambda: True
Jack Rosenthal95aac172022-06-30 15:35:07 -06001553
Alex Klein1699fab2022-09-08 08:46:06 -06001554 # In case the action wants to throw a parser error.
1555 opts.parser = parser
Mike Frysinger484e2f82020-03-20 01:41:10 -04001556
Alex Klein1699fab2022-09-08 08:46:06 -06001557 # A cache of gerrit helpers we'll load on demand.
1558 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001559
Alex Klein1699fab2022-09-08 08:46:06 -06001560 if opts.format is OutputFormat.AUTO:
1561 if sys.stdout.isatty():
1562 opts.format = OutputFormat.PRETTY
1563 else:
1564 opts.format = OutputFormat.RAW
Jack Rosenthale3a92672022-06-29 14:54:48 -06001565
Alex Klein1699fab2022-09-08 08:46:06 -06001566 opts.Freeze()
Mike Frysinger88f27292014-06-17 09:40:45 -07001567
Alex Klein1699fab2022-09-08 08:46:06 -06001568 # pylint: disable=global-statement
1569 global COLOR
1570 COLOR = terminal.Color(enabled=opts.color)
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001571
Alex Klein1699fab2022-09-08 08:46:06 -06001572 # Now look up the requested user action and run it.
1573 actions = _GetActions()
1574 action_class = actions[opts.action]
1575 if action_class.USE_PAGER and opts.pager:
1576 start_pager()
1577 obj = action_class()
1578 try:
1579 obj(opts)
1580 except (
1581 cros_build_lib.RunCommandError,
1582 gerrit.GerritException,
1583 gob_util.GOBError,
1584 ) as e:
1585 cros_build_lib.Die(e)