blob: 889d24a7b33ab5084a84ab728a88b85f076d711d [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2012 The ChromiumOS Authors
Mike Frysinger13f23a42013-05-13 17:32:01 -04002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Mike Frysinger08737512014-02-07 22:58:26 -05005"""A command line interface to Gerrit-on-borg instances.
Mike Frysinger13f23a42013-05-13 17:32:01 -04006
7Internal Note:
8To expose a function directly to the command line interface, name your function
9with the prefix "UserAct".
10"""
11
Mike Frysinger8037f752020-02-29 20:47:09 -050012import argparse
Mike Frysinger65fc8632020-02-06 18:11:12 -050013import collections
Mike Frysinger2295d792021-03-08 15:55:23 -050014import configparser
Jack Rosenthale3a92672022-06-29 14:54:48 -060015import enum
Mike Frysingerc7796cf2020-02-06 23:55:15 -050016import functools
Mike Frysinger13f23a42013-05-13 17:32:01 -040017import inspect
Mike Frysinger87c74ce2017-04-04 16:12:31 -040018import json
Chris McDonald59650c32021-07-20 15:29:28 -060019import logging
Jack Rosenthal95aac172022-06-30 15:35:07 -060020import os
Mike Frysinger2295d792021-03-08 15:55:23 -050021from pathlib import Path
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070022import re
Mike Frysinger2295d792021-03-08 15:55:23 -050023import shlex
Jack Rosenthal95aac172022-06-30 15:35:07 -060024import signal
25import subprocess
Mike Frysinger87c74ce2017-04-04 16:12:31 -040026import sys
Mike Frysinger32c1d9f2023-06-22 09:07:19 -040027from typing import List, Optional, Set, Tuple
Mike Frysinger13f23a42013-05-13 17:32:01 -040028
Mike Frysinger2295d792021-03-08 15:55:23 -050029from chromite.lib import chromite_config
Chris McDonald59650c32021-07-20 15:29:28 -060030from chromite.lib import commandline
Aviv Keshetb7519e12016-10-04 00:50:00 -070031from chromite.lib import config_lib
32from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040033from chromite.lib import cros_build_lib
34from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050035from chromite.lib import gob_util
Mike Frysinger254f33f2019-12-11 13:54:29 -050036from chromite.lib import parallel
Mike Frysingera9751c92021-04-30 10:12:37 -040037from chromite.lib import retry_util
Mike Frysinger13f23a42013-05-13 17:32:01 -040038from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040039from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060040from chromite.utils import memoize
Alex Klein73eba212021-09-09 11:43:33 -060041from chromite.utils import pformat
Mike Frysinger13f23a42013-05-13 17:32:01 -040042
43
Mike Frysinger2295d792021-03-08 15:55:23 -050044class Config:
Alex Klein1699fab2022-09-08 08:46:06 -060045 """Manage the user's gerrit config settings.
Mike Frysinger2295d792021-03-08 15:55:23 -050046
Alex Klein1699fab2022-09-08 08:46:06 -060047 This is entirely unique to this gerrit command. Inspiration for naming and
48 layout is taken from ~/.gitconfig settings.
49 """
Mike Frysinger2295d792021-03-08 15:55:23 -050050
Alex Klein1699fab2022-09-08 08:46:06 -060051 def __init__(self, path: Path = chromite_config.GERRIT_CONFIG):
52 self.cfg = configparser.ConfigParser(interpolation=None)
53 if path.exists():
54 self.cfg.read(chromite_config.GERRIT_CONFIG)
Mike Frysinger2295d792021-03-08 15:55:23 -050055
Alex Klein1699fab2022-09-08 08:46:06 -060056 def expand_alias(self, action):
57 """Expand any aliases."""
58 alias = self.cfg.get("alias", action, fallback=None)
59 if alias is not None:
60 return shlex.split(alias)
61 return action
Mike Frysinger2295d792021-03-08 15:55:23 -050062
63
Alex Klein074f94f2023-06-22 10:32:06 -060064class UserAction:
Alex Klein1699fab2022-09-08 08:46:06 -060065 """Base class for all custom user actions."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -050066
Alex Klein1699fab2022-09-08 08:46:06 -060067 # The name of the command the user types in.
68 COMMAND = None
Mike Frysingerc7796cf2020-02-06 23:55:15 -050069
Alex Klein1699fab2022-09-08 08:46:06 -060070 # Should output be paged?
71 USE_PAGER = False
Jack Rosenthal95aac172022-06-30 15:35:07 -060072
Alex Klein1699fab2022-09-08 08:46:06 -060073 @staticmethod
74 def init_subparser(parser):
75 """Add arguments to this action's subparser."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -050076
Alex Klein1699fab2022-09-08 08:46:06 -060077 @staticmethod
78 def __call__(opts):
79 """Implement the action."""
80 raise RuntimeError(
81 "Internal error: action missing __call__ implementation"
82 )
Mike Frysinger108eda22018-06-06 18:45:12 -040083
84
Mike Frysinger254f33f2019-12-11 13:54:29 -050085# How many connections we'll use in parallel. We don't want this to be too high
86# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
87# seems to be good enough for users.
88CONNECTION_LIMIT = 10
89
90
Mike Frysinger031ad0b2013-05-14 18:15:34 -040091COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040092
93# Map the internal names to the ones we normally show on the web ui.
94GERRIT_APPROVAL_MAP = {
Alex Klein1699fab2022-09-08 08:46:06 -060095 "COMR": [
96 "CQ",
97 "Commit Queue ",
98 ],
99 "CRVW": [
100 "CR",
101 "Code Review ",
102 ],
103 "SUBM": [
104 "S ",
105 "Submitted ",
106 ],
107 "VRIF": [
108 "V ",
109 "Verified ",
110 ],
111 "LCQ": [
112 "L ",
113 "Legacy ",
114 ],
Mike Frysinger13f23a42013-05-13 17:32:01 -0400115}
116
117# Order is important -- matches the web ui. This also controls the short
118# entries that we summarize in non-verbose mode.
Alex Klein1699fab2022-09-08 08:46:06 -0600119GERRIT_SUMMARY_CATS = (
120 "CR",
121 "CQ",
122 "V",
123)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400124
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400125# Shorter strings for CL status messages.
126GERRIT_SUMMARY_MAP = {
Alex Klein1699fab2022-09-08 08:46:06 -0600127 "ABANDONED": "ABD",
128 "MERGED": "MRG",
129 "NEW": "NEW",
130 "WIP": "WIP",
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400131}
132
Mike Frysinger13f23a42013-05-13 17:32:01 -0400133
Jack Rosenthale3a92672022-06-29 14:54:48 -0600134class OutputFormat(enum.Enum):
Alex Klein1699fab2022-09-08 08:46:06 -0600135 """Type for the requested output format.
Jack Rosenthale3a92672022-06-29 14:54:48 -0600136
Alex Klein1699fab2022-09-08 08:46:06 -0600137 AUTO: Automatically determine the format based on what the user
138 might want. This is PRETTY if attached to a terminal, RAW
139 otherwise.
140 RAW: Output CLs one per line, suitable for mild scripting.
141 JSON: JSON-encoded output, suitable for spicy scripting.
142 MARKDOWN: Suitable for posting in a bug or CL comment.
143 PRETTY: Suitable for viewing in a color terminal.
144 """
145
146 AUTO = 0
147 AUTOMATIC = AUTO
148 RAW = 1
149 JSON = 2
150 MARKDOWN = 3
151 PRETTY = 4
Jack Rosenthale3a92672022-06-29 14:54:48 -0600152
153
Mike Frysinger13f23a42013-05-13 17:32:01 -0400154def red(s):
Alex Klein1699fab2022-09-08 08:46:06 -0600155 return COLOR.Color(terminal.Color.RED, s)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400156
157
158def green(s):
Alex Klein1699fab2022-09-08 08:46:06 -0600159 return COLOR.Color(terminal.Color.GREEN, s)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400160
161
162def blue(s):
Alex Klein1699fab2022-09-08 08:46:06 -0600163 return COLOR.Color(terminal.Color.BLUE, s)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400164
165
Mike 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:
Mike Frysingerd74a8bc2023-06-22 09:42:55 -0400213 items: Items that begin with "~" or "-" mean "remove" while others are
214 "add".
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400215 validate: A regular expression to validate each item.
216
217 Returns:
218 A tuple of sets: all the items to add and all the items to remove.
Mike Frysingerd74a8bc2023-06-22 09:42:55 -0400219 NB: The leading "~" & "-" will automatically be stripped.
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400220 """
221 validator = re.compile(validate) if validate else None
222
223 add_list, remove_list, invalid_list = set(), set(), set()
224 for item in items:
225 if not item:
226 invalid_list.add(item)
227 continue
228
229 remove = False
Mike Frysingerd74a8bc2023-06-22 09:42:55 -0400230 if item[0] in ("~", "-"):
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400231 remove = True
232 item = item[1:]
233
234 if validator and not validator.match(item):
235 invalid_list.add(item)
236 elif remove:
237 remove_list.add(item)
238 add_list.discard(item)
239 else:
240 add_list.add(item)
241 remove_list.discard(item)
242
243 if invalid_list:
244 cros_build_lib.Die("Invalid arguments: %s", ", ".join(invalid_list))
245
246 return (add_list, remove_list)
247
248
Mike Frysinger88f27292014-06-17 09:40:45 -0700249# TODO: This func really needs to be merged into the core gerrit logic.
250def GetGerrit(opts, cl=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600251 """Auto pick the right gerrit instance based on the |cl|
Mike Frysinger88f27292014-06-17 09:40:45 -0700252
Alex Klein1699fab2022-09-08 08:46:06 -0600253 Args:
Trent Apted66736d82023-05-25 10:38:28 +1000254 opts: The general options object.
255 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
Mike Frysinger88f27292014-06-17 09:40:45 -0700256
Alex Klein1699fab2022-09-08 08:46:06 -0600257 Returns:
Trent Apted66736d82023-05-25 10:38:28 +1000258 A tuple of a gerrit object and a sanitized CL #.
Alex Klein1699fab2022-09-08 08:46:06 -0600259 """
260 gob = opts.gob
261 if cl is not None:
262 if cl.startswith("*") or cl.startswith("chrome-internal:"):
263 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
264 if cl.startswith("*"):
265 cl = cl[1:]
266 else:
267 cl = cl[16:]
268 elif ":" in cl:
269 gob, cl = cl.split(":", 1)
Mike Frysinger88f27292014-06-17 09:40:45 -0700270
Alex Klein1699fab2022-09-08 08:46:06 -0600271 if not gob in opts.gerrit:
272 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
Mike Frysinger88f27292014-06-17 09:40:45 -0700273
Alex Klein1699fab2022-09-08 08:46:06 -0600274 return (opts.gerrit[gob], cl)
Mike Frysinger88f27292014-06-17 09:40:45 -0700275
276
Mike Frysinger13f23a42013-05-13 17:32:01 -0400277def GetApprovalSummary(_opts, cls):
Alex Klein1699fab2022-09-08 08:46:06 -0600278 """Return a dict of the most important approvals"""
Alex Kleine37b8762023-04-17 12:16:15 -0600279 approvs = {x: "" for x in GERRIT_SUMMARY_CATS}
Alex Klein1699fab2022-09-08 08:46:06 -0600280 for approver in cls.get("currentPatchSet", {}).get("approvals", []):
281 cats = GERRIT_APPROVAL_MAP.get(approver["type"])
282 if not cats:
283 logging.warning(
284 "unknown gerrit approval type: %s", approver["type"]
285 )
286 continue
287 cat = cats[0].strip()
288 val = int(approver["value"])
289 if not cat in approvs:
290 # Ignore the extended categories in the summary view.
291 continue
292 elif approvs[cat] == "":
293 approvs[cat] = val
294 elif val < 0:
295 approvs[cat] = min(approvs[cat], val)
296 else:
297 approvs[cat] = max(approvs[cat], val)
298 return approvs
Mike Frysinger13f23a42013-05-13 17:32:01 -0400299
300
Mike Frysingera1b4b272017-04-05 16:11:00 -0400301def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600302 """Pretty print a single result"""
303 if lims is None:
304 lims = {"url": 0, "project": 0}
Mike Frysinger13f23a42013-05-13 17:32:01 -0400305
Alex Klein1699fab2022-09-08 08:46:06 -0600306 status = ""
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400307
Alex Klein1699fab2022-09-08 08:46:06 -0600308 if opts.verbose:
309 status += "%s " % (cl["status"],)
310 else:
311 status += "%s " % (GERRIT_SUMMARY_MAP.get(cl["status"], cl["status"]),)
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400312
Alex Klein1699fab2022-09-08 08:46:06 -0600313 if show_approvals and not opts.verbose:
314 approvs = GetApprovalSummary(opts, cl)
315 for cat in GERRIT_SUMMARY_CATS:
316 if approvs[cat] in ("", 0):
317 functor = lambda x: x
318 elif approvs[cat] < 0:
319 functor = red
320 else:
321 functor = green
322 status += functor("%s:%2s " % (cat, approvs[cat]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400323
Alex Klein1699fab2022-09-08 08:46:06 -0600324 if opts.format is OutputFormat.MARKDOWN:
325 print("* %s - %s" % (uri_lib.ShortenUri(cl["url"]), cl["subject"]))
326 else:
327 print(
328 "%s %s%-*s %s"
329 % (
330 blue("%-*s" % (lims["url"], cl["url"])),
331 status,
332 lims["project"],
333 cl["project"],
334 cl["subject"],
335 )
336 )
Mike Frysinger13f23a42013-05-13 17:32:01 -0400337
Alex Klein1699fab2022-09-08 08:46:06 -0600338 if show_approvals and opts.verbose:
339 for approver in cl["currentPatchSet"].get("approvals", []):
340 functor = red if int(approver["value"]) < 0 else green
341 n = functor("%2s" % approver["value"])
342 t = GERRIT_APPROVAL_MAP.get(
343 approver["type"], [approver["type"], approver["type"]]
344 )[1]
345 print(" %s %s %s" % (n, t, approver["by"]["email"]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400346
347
Mike Frysingera1b4b272017-04-05 16:11:00 -0400348def PrintCls(opts, cls, lims=None, show_approvals=True):
Alex Klein1699fab2022-09-08 08:46:06 -0600349 """Print all results based on the requested format."""
350 if opts.format is OutputFormat.RAW:
351 site_params = config_lib.GetSiteParams()
352 pfx = ""
353 # Special case internal Chrome GoB as that is what most devs use.
354 # They can always redirect the list elsewhere via the -g option.
355 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
356 pfx = site_params.INTERNAL_CHANGE_PREFIX
357 for cl in cls:
358 print("%s%s" % (pfx, cl["number"]))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400359
Alex Klein1699fab2022-09-08 08:46:06 -0600360 elif opts.format is OutputFormat.JSON:
361 json.dump(cls, sys.stdout)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400362
Alex Klein1699fab2022-09-08 08:46:06 -0600363 else:
364 if lims is None:
365 lims = limits(cls)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400366
Alex Klein1699fab2022-09-08 08:46:06 -0600367 for cl in cls:
368 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400369
370
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400371def _Query(opts, query, raw=True, helper=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600372 """Queries Gerrit with a query string built from the commandline options"""
373 if opts.branch is not None:
374 query += " branch:%s" % opts.branch
375 if opts.project is not None:
376 query += " project: %s" % opts.project
377 if opts.topic is not None:
378 query += " topic: %s" % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800379
Alex Klein1699fab2022-09-08 08:46:06 -0600380 if helper is None:
381 helper, _ = GetGerrit(opts)
382 return helper.Query(query, raw=raw, bypass_cache=False)
Paul Hobbs89765232015-06-24 14:07:49 -0700383
384
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400385def FilteredQuery(opts, query, helper=None):
Alex Klein1699fab2022-09-08 08:46:06 -0600386 """Query gerrit and filter/clean up the results"""
387 ret = []
Paul Hobbs89765232015-06-24 14:07:49 -0700388
Alex Klein1699fab2022-09-08 08:46:06 -0600389 logging.debug("Running query: %s", query)
390 for cl in _Query(opts, query, raw=True, helper=helper):
391 # Gerrit likes to return a stats record too.
392 if not "project" in cl:
393 continue
Mike Frysinger13f23a42013-05-13 17:32:01 -0400394
Alex Klein1699fab2022-09-08 08:46:06 -0600395 # Strip off common leading names since the result is still
396 # unique over the whole tree.
397 if not opts.verbose:
398 for pfx in (
399 "aosp",
400 "chromeos",
401 "chromiumos",
402 "external",
403 "overlays",
404 "platform",
405 "third_party",
406 ):
407 if cl["project"].startswith("%s/" % pfx):
408 cl["project"] = cl["project"][len(pfx) + 1 :]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400409
Alex Klein1699fab2022-09-08 08:46:06 -0600410 cl["url"] = uri_lib.ShortenUri(cl["url"])
Mike Frysinger479f1192017-09-14 22:36:30 -0400411
Alex Klein1699fab2022-09-08 08:46:06 -0600412 ret.append(cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400413
Alex Klein1699fab2022-09-08 08:46:06 -0600414 if opts.sort == "unsorted":
415 return ret
416 if opts.sort == "number":
417 key = lambda x: int(x[opts.sort])
418 else:
419 key = lambda x: x[opts.sort]
420 return sorted(ret, key=key)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400421
422
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500423class _ActionSearchQuery(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600424 """Base class for actions that perform searches."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500425
Alex Klein1699fab2022-09-08 08:46:06 -0600426 USE_PAGER = True
Jack Rosenthal95aac172022-06-30 15:35:07 -0600427
Alex Klein1699fab2022-09-08 08:46:06 -0600428 @staticmethod
429 def init_subparser(parser):
430 """Add arguments to this action's subparser."""
431 parser.add_argument(
432 "--sort",
433 default="number",
Trent Apted66736d82023-05-25 10:38:28 +1000434 help='Key to sort on (number, project); use "unsorted" to disable',
Alex Klein1699fab2022-09-08 08:46:06 -0600435 )
436 parser.add_argument(
437 "-b", "--branch", help="Limit output to the specific branch"
438 )
439 parser.add_argument(
440 "-p", "--project", help="Limit output to the specific project"
441 )
442 parser.add_argument(
443 "-t", "--topic", help="Limit output to the specific topic"
444 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500445
446
447class ActionTodo(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600448 """List CLs needing your review"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500449
Alex Klein1699fab2022-09-08 08:46:06 -0600450 COMMAND = "todo"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500451
Alex Klein1699fab2022-09-08 08:46:06 -0600452 @staticmethod
453 def __call__(opts):
454 """Implement the action."""
455 cls = FilteredQuery(opts, "attention:self")
456 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400457
458
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500459class ActionSearch(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600460 """List CLs matching the search query"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500461
Alex Klein1699fab2022-09-08 08:46:06 -0600462 COMMAND = "search"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500463
Alex Klein1699fab2022-09-08 08:46:06 -0600464 @staticmethod
465 def init_subparser(parser):
466 """Add arguments to this action's subparser."""
467 _ActionSearchQuery.init_subparser(parser)
468 parser.add_argument("query", help="The search query")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500469
Alex Klein1699fab2022-09-08 08:46:06 -0600470 @staticmethod
471 def __call__(opts):
472 """Implement the action."""
473 cls = FilteredQuery(opts, opts.query)
474 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400475
476
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500477class ActionMine(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600478 """List your CLs with review statuses"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500479
Alex Klein1699fab2022-09-08 08:46:06 -0600480 COMMAND = "mine"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500481
Alex Klein1699fab2022-09-08 08:46:06 -0600482 @staticmethod
483 def init_subparser(parser):
484 """Add arguments to this action's subparser."""
485 _ActionSearchQuery.init_subparser(parser)
486 parser.add_argument(
487 "--draft",
488 default=False,
489 action="store_true",
490 help="Show draft changes",
491 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500492
Alex Klein1699fab2022-09-08 08:46:06 -0600493 @staticmethod
494 def __call__(opts):
495 """Implement the action."""
496 if opts.draft:
497 rule = "is:draft"
498 else:
499 rule = "status:new"
500 cls = FilteredQuery(opts, "owner:self %s" % (rule,))
501 PrintCls(opts, cls)
Mike Frysingera1db2c42014-06-15 00:42:48 -0700502
503
Paul Hobbs89765232015-06-24 14:07:49 -0700504def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
Alex Klein1699fab2022-09-08 08:46:06 -0600505 """Runs breadth first search starting from the nodes in |to_visit|
Paul Hobbs89765232015-06-24 14:07:49 -0700506
Alex Klein1699fab2022-09-08 08:46:06 -0600507 Args:
Trent Apted66736d82023-05-25 10:38:28 +1000508 to_visit: the starting nodes
509 children: a function which takes a node and returns the adjacent nodes
510 visited_key: a function for deduplicating node visits. Defaults to the
511 identity function (lambda x: x)
Paul Hobbs89765232015-06-24 14:07:49 -0700512
Alex Klein1699fab2022-09-08 08:46:06 -0600513 Returns:
Trent Apted66736d82023-05-25 10:38:28 +1000514 A list of nodes which are reachable from any node in |to_visit| by
515 calling
516 |children| any number of times.
Alex Klein1699fab2022-09-08 08:46:06 -0600517 """
518 to_visit = list(to_visit)
519 seen = set(visited_key(x) for x in to_visit)
520 for node in to_visit:
521 for child in children(node):
522 key = visited_key(child)
523 if key not in seen:
524 seen.add(key)
525 to_visit.append(child)
526 return to_visit
Paul Hobbs89765232015-06-24 14:07:49 -0700527
528
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500529class ActionDeps(_ActionSearchQuery):
Alex Klein4507b172023-01-13 11:39:51 -0700530 """List CLs matching a query, and transitive dependencies of those CLs."""
Paul Hobbs89765232015-06-24 14:07:49 -0700531
Alex Klein1699fab2022-09-08 08:46:06 -0600532 COMMAND = "deps"
Paul Hobbs89765232015-06-24 14:07:49 -0700533
Alex Klein1699fab2022-09-08 08:46:06 -0600534 @staticmethod
535 def init_subparser(parser):
536 """Add arguments to this action's subparser."""
537 _ActionSearchQuery.init_subparser(parser)
538 parser.add_argument("query", help="The search query")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500539
Alex Klein1699fab2022-09-08 08:46:06 -0600540 def __call__(self, opts):
541 """Implement the action."""
542 cls = _Query(opts, opts.query, raw=False)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500543
Alex Klein1699fab2022-09-08 08:46:06 -0600544 @memoize.Memoize
545 def _QueryChange(cl, helper=None):
546 return _Query(opts, cl, raw=False, helper=helper)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500547
Alex Klein1699fab2022-09-08 08:46:06 -0600548 transitives = _BreadthFirstSearch(
549 cls,
550 functools.partial(self._Children, opts, _QueryChange),
551 visited_key=lambda cl: cl.PatchLink(),
552 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500553
Alex Klein1699fab2022-09-08 08:46:06 -0600554 # This is a hack to avoid losing GoB host for each CL. The PrintCls
555 # function assumes the GoB host specified by the user is the only one
556 # that is ever used, but the deps command walks across hosts.
557 if opts.format is OutputFormat.RAW:
558 print("\n".join(x.PatchLink() for x in transitives))
559 else:
560 transitives_raw = [cl.patch_dict for cl in transitives]
561 PrintCls(opts, transitives_raw)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500562
Alex Klein1699fab2022-09-08 08:46:06 -0600563 @staticmethod
564 def _ProcessDeps(opts, querier, cl, deps, required):
565 """Yields matching dependencies for a patch"""
566 # We need to query the change to guarantee that we have a .gerrit_number
567 for dep in deps:
568 if not dep.remote in opts.gerrit:
569 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
570 remote=dep.remote, print_cmd=opts.debug
571 )
572 helper = opts.gerrit[dep.remote]
Mike Frysingerb3300c42017-07-20 01:41:17 -0400573
Alex Klein1699fab2022-09-08 08:46:06 -0600574 # TODO(phobbs) this should maybe catch network errors.
575 changes = querier(dep.ToGerritQueryText(), helper=helper)
Mike Frysinger5726da92017-09-20 22:14:25 -0400576
Alex Klein4507b172023-01-13 11:39:51 -0700577 # Handle empty results. If we found a commit that was pushed
578 # directly (e.g. a bot commit), then gerrit won't know about it.
Alex Klein1699fab2022-09-08 08:46:06 -0600579 if not changes:
580 if required:
581 logging.error(
582 "CL %s depends on %s which cannot be found",
583 cl,
584 dep.ToGerritQueryText(),
585 )
586 continue
Mike Frysinger5726da92017-09-20 22:14:25 -0400587
Alex Klein4507b172023-01-13 11:39:51 -0700588 # Our query might have matched more than one result. This can come
589 # up when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id
590 # shows up across multiple repos/branches. We blindly check all of
591 # them in the hopes that all open ones are what the user wants, but
592 # then again the CQ-DEPEND syntax itself is unable to differentiate.
593 # *shrug*
Alex Klein1699fab2022-09-08 08:46:06 -0600594 if len(changes) > 1:
595 logging.warning(
596 "CL %s has an ambiguous CQ dependency %s",
597 cl,
598 dep.ToGerritQueryText(),
599 )
600 for change in changes:
601 if change.status == "NEW":
602 yield change
Mike Frysinger5726da92017-09-20 22:14:25 -0400603
Alex Klein1699fab2022-09-08 08:46:06 -0600604 @classmethod
605 def _Children(cls, opts, querier, cl):
606 """Yields the Gerrit dependencies of a patch"""
607 for change in cls._ProcessDeps(
608 opts, querier, cl, cl.GerritDependencies(), False
609 ):
610 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700611
Paul Hobbs89765232015-06-24 14:07:49 -0700612
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500613class ActionInspect(_ActionSearchQuery):
Alex Klein1699fab2022-09-08 08:46:06 -0600614 """Show the details of one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500615
Alex Klein1699fab2022-09-08 08:46:06 -0600616 COMMAND = "inspect"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500617
Alex Klein1699fab2022-09-08 08:46:06 -0600618 @staticmethod
619 def init_subparser(parser):
620 """Add arguments to this action's subparser."""
621 _ActionSearchQuery.init_subparser(parser)
622 parser.add_argument(
623 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
624 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500625
Alex Klein1699fab2022-09-08 08:46:06 -0600626 @staticmethod
627 def __call__(opts):
628 """Implement the action."""
629 cls = []
630 for arg in opts.cls:
631 helper, cl = GetGerrit(opts, arg)
632 change = FilteredQuery(opts, "change:%s" % cl, helper=helper)
633 if change:
634 cls.extend(change)
635 else:
636 logging.warning("no results found for CL %s", arg)
637 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400638
639
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500640class _ActionLabeler(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600641 """Base helper for setting labels."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500642
Alex Klein1699fab2022-09-08 08:46:06 -0600643 LABEL = None
644 VALUES = None
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500645
Alex Klein1699fab2022-09-08 08:46:06 -0600646 @classmethod
647 def init_subparser(cls, parser):
648 """Add arguments to this action's subparser."""
649 parser.add_argument(
650 "-m",
651 "--msg",
652 "--message",
653 metavar="MESSAGE",
654 help="Optional message to include",
655 )
656 parser.add_argument(
657 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
658 )
659 parser.add_argument(
660 "value",
661 nargs=1,
662 metavar="value",
663 choices=cls.VALUES,
664 help="The label value; one of [%(choices)s]",
665 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500666
Alex Klein1699fab2022-09-08 08:46:06 -0600667 @classmethod
668 def __call__(cls, opts):
669 """Implement the action."""
Mike Frysinger16474792023-03-01 01:18:00 -0500670
Alex Klein1699fab2022-09-08 08:46:06 -0600671 # Convert user-friendly command line option into a gerrit parameter.
672 def task(arg):
673 helper, cl = GetGerrit(opts, arg)
674 helper.SetReview(
675 cl,
676 labels={cls.LABEL: opts.value[0]},
677 msg=opts.msg,
678 dryrun=opts.dryrun,
679 notify=opts.notify,
680 )
681
682 _run_parallel_tasks(task, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500683
684
685class ActionLabelAutoSubmit(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600686 """Change the Auto-Submit label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500687
Alex Klein1699fab2022-09-08 08:46:06 -0600688 COMMAND = "label-as"
689 LABEL = "Auto-Submit"
690 VALUES = ("0", "1")
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600691
692
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500693class ActionLabelCodeReview(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600694 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500695
Alex Klein1699fab2022-09-08 08:46:06 -0600696 COMMAND = "label-cr"
697 LABEL = "Code-Review"
698 VALUES = ("-2", "-1", "0", "1", "2")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400699
700
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500701class ActionLabelVerified(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600702 """Change the Verified label"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500703
Alex Klein1699fab2022-09-08 08:46:06 -0600704 COMMAND = "label-v"
705 LABEL = "Verified"
706 VALUES = ("-1", "0", "1")
Mike Frysinger13f23a42013-05-13 17:32:01 -0400707
708
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500709class ActionLabelCommitQueue(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600710 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500711
Alex Klein1699fab2022-09-08 08:46:06 -0600712 COMMAND = "label-cq"
713 LABEL = "Commit-Queue"
714 VALUES = ("0", "1", "2")
715
Mike Frysinger15b23e42014-12-05 17:00:05 -0500716
C Shapiro3f1f8242021-08-02 15:28:29 -0500717class ActionLabelOwnersOverride(_ActionLabeler):
Alex Klein1699fab2022-09-08 08:46:06 -0600718 """Change the Owners-Override label (1=Override)"""
C Shapiro3f1f8242021-08-02 15:28:29 -0500719
Alex Klein1699fab2022-09-08 08:46:06 -0600720 COMMAND = "label-oo"
721 LABEL = "Owners-Override"
722 VALUES = ("0", "1")
C Shapiro3f1f8242021-08-02 15:28:29 -0500723
Mike Frysinger15b23e42014-12-05 17:00:05 -0500724
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500725class _ActionSimpleParallelCLs(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600726 """Base helper for actions that only accept CLs."""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500727
Alex Klein1699fab2022-09-08 08:46:06 -0600728 @staticmethod
729 def init_subparser(parser):
730 """Add arguments to this action's subparser."""
731 parser.add_argument(
732 "cls", nargs="+", metavar="CL", help="The CL(s) to update"
733 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500734
Alex Klein1699fab2022-09-08 08:46:06 -0600735 def __call__(self, opts):
736 """Implement the action."""
737
738 def task(arg):
739 helper, cl = GetGerrit(opts, arg)
740 self._process_one(helper, cl, opts)
741
742 _run_parallel_tasks(task, *opts.cls)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500743
744
745class ActionSubmit(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600746 """Submit CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500747
Alex Klein1699fab2022-09-08 08:46:06 -0600748 COMMAND = "submit"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500749
Alex Klein1699fab2022-09-08 08:46:06 -0600750 @staticmethod
751 def _process_one(helper, cl, opts):
752 """Use |helper| to process the single |cl|."""
753 helper.SubmitChange(cl, dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400754
755
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500756class ActionAbandon(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600757 """Abandon CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500758
Alex Klein1699fab2022-09-08 08:46:06 -0600759 COMMAND = "abandon"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500760
Alex Klein1699fab2022-09-08 08:46:06 -0600761 @staticmethod
762 def init_subparser(parser):
763 """Add arguments to this action's subparser."""
764 parser.add_argument(
765 "-m",
766 "--msg",
767 "--message",
768 metavar="MESSAGE",
769 help="Include a message",
770 )
771 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger3af378b2021-03-12 01:34:04 -0500772
Alex Klein1699fab2022-09-08 08:46:06 -0600773 @staticmethod
774 def _process_one(helper, cl, opts):
775 """Use |helper| to process the single |cl|."""
776 helper.AbandonChange(
777 cl, msg=opts.msg, dryrun=opts.dryrun, notify=opts.notify
778 )
Mike Frysinger13f23a42013-05-13 17:32:01 -0400779
780
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500781class ActionRestore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600782 """Restore CLs that were abandoned"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500783
Alex Klein1699fab2022-09-08 08:46:06 -0600784 COMMAND = "restore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500785
Alex Klein1699fab2022-09-08 08:46:06 -0600786 @staticmethod
787 def _process_one(helper, cl, opts):
788 """Use |helper| to process the single |cl|."""
789 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400790
791
Tomasz Figa54d70992021-01-20 13:48:59 +0900792class ActionWorkInProgress(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600793 """Mark CLs as work in progress"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900794
Alex Klein1699fab2022-09-08 08:46:06 -0600795 COMMAND = "wip"
Tomasz Figa54d70992021-01-20 13:48:59 +0900796
Alex Klein1699fab2022-09-08 08:46:06 -0600797 @staticmethod
798 def _process_one(helper, cl, opts):
799 """Use |helper| to process the single |cl|."""
800 helper.SetWorkInProgress(cl, True, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900801
802
803class ActionReadyForReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600804 """Mark CLs as ready for review"""
Tomasz Figa54d70992021-01-20 13:48:59 +0900805
Alex Klein1699fab2022-09-08 08:46:06 -0600806 COMMAND = "ready"
Tomasz Figa54d70992021-01-20 13:48:59 +0900807
Alex Klein1699fab2022-09-08 08:46:06 -0600808 @staticmethod
809 def _process_one(helper, cl, opts):
810 """Use |helper| to process the single |cl|."""
811 helper.SetWorkInProgress(cl, False, dryrun=opts.dryrun)
Tomasz Figa54d70992021-01-20 13:48:59 +0900812
813
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500814class ActionReviewers(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600815 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700816
Alex Klein1699fab2022-09-08 08:46:06 -0600817 COMMAND = "reviewers"
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700818
Alex Klein1699fab2022-09-08 08:46:06 -0600819 @staticmethod
820 def init_subparser(parser):
821 """Add arguments to this action's subparser."""
822 parser.add_argument("cl", metavar="CL", help="The CL to update")
823 parser.add_argument(
824 "reviewers", nargs="+", help="The reviewers to add/remove"
825 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700826
Alex Klein1699fab2022-09-08 08:46:06 -0600827 @staticmethod
828 def __call__(opts):
829 """Implement the action."""
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400830 add_list, remove_list = process_add_remove_lists(
831 opts.reviewers, f"^{constants.EMAIL_REGEX}$"
832 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500833
Alex Klein1699fab2022-09-08 08:46:06 -0600834 if add_list or remove_list:
835 helper, cl = GetGerrit(opts, opts.cl)
836 helper.SetReviewers(
837 cl,
838 add=add_list,
839 remove=remove_list,
840 dryrun=opts.dryrun,
841 notify=opts.notify,
842 )
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700843
844
Brian Norrisd25af082021-10-29 11:25:31 -0700845class ActionAttentionSet(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600846 """Add/remove emails from the attention set (prepend with '~' to remove)"""
Brian Norrisd25af082021-10-29 11:25:31 -0700847
Alex Klein1699fab2022-09-08 08:46:06 -0600848 COMMAND = "attention"
Brian Norrisd25af082021-10-29 11:25:31 -0700849
Alex Klein1699fab2022-09-08 08:46:06 -0600850 @staticmethod
851 def init_subparser(parser):
852 """Add arguments to this action's subparser."""
853 parser.add_argument(
854 "-m",
855 "--msg",
856 "--message",
857 metavar="MESSAGE",
858 help="Optional message to include",
859 default="gerrit CLI",
860 )
861 parser.add_argument("cl", metavar="CL", help="The CL to update")
862 parser.add_argument(
863 "users",
864 nargs="+",
865 help="The users to add/remove from attention set",
866 )
Brian Norrisd25af082021-10-29 11:25:31 -0700867
Alex Klein1699fab2022-09-08 08:46:06 -0600868 @staticmethod
869 def __call__(opts):
870 """Implement the action."""
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400871 add_list, remove_list = process_add_remove_lists(
872 opts.users, f"^{constants.EMAIL_REGEX}$"
873 )
Brian Norrisd25af082021-10-29 11:25:31 -0700874
Alex Klein1699fab2022-09-08 08:46:06 -0600875 if add_list or remove_list:
876 helper, cl = GetGerrit(opts, opts.cl)
877 helper.SetAttentionSet(
878 cl,
879 add=add_list,
880 remove=remove_list,
881 dryrun=opts.dryrun,
882 notify=opts.notify,
883 message=opts.msg,
884 )
Brian Norrisd25af082021-10-29 11:25:31 -0700885
886
Mike Frysinger62178ae2020-03-20 01:37:43 -0400887class ActionMessage(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600888 """Add a message to a CL"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500889
Alex Klein1699fab2022-09-08 08:46:06 -0600890 COMMAND = "message"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500891
Alex Klein1699fab2022-09-08 08:46:06 -0600892 @staticmethod
893 def init_subparser(parser):
894 """Add arguments to this action's subparser."""
895 _ActionSimpleParallelCLs.init_subparser(parser)
896 parser.add_argument("message", help="The message to post")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500897
Alex Klein1699fab2022-09-08 08:46:06 -0600898 @staticmethod
899 def _process_one(helper, cl, opts):
900 """Use |helper| to process the single |cl|."""
901 helper.SetReview(cl, msg=opts.message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530902
903
Mike Frysinger62178ae2020-03-20 01:37:43 -0400904class ActionTopic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600905 """Set a topic for one or more CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500906
Alex Klein1699fab2022-09-08 08:46:06 -0600907 COMMAND = "topic"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500908
Alex Klein1699fab2022-09-08 08:46:06 -0600909 @staticmethod
910 def init_subparser(parser):
911 """Add arguments to this action's subparser."""
912 _ActionSimpleParallelCLs.init_subparser(parser)
913 parser.add_argument("topic", help="The topic to set")
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500914
Alex Klein1699fab2022-09-08 08:46:06 -0600915 @staticmethod
916 def _process_one(helper, cl, opts):
917 """Use |helper| to process the single |cl|."""
918 helper.SetTopic(cl, opts.topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800919
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800920
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500921class ActionPrivate(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600922 """Mark CLs private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700923
Alex Klein1699fab2022-09-08 08:46:06 -0600924 COMMAND = "private"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500925
Alex Klein1699fab2022-09-08 08:46:06 -0600926 @staticmethod
927 def _process_one(helper, cl, opts):
928 """Use |helper| to process the single |cl|."""
929 helper.SetPrivate(cl, True, dryrun=opts.dryrun)
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700930
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800931
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500932class ActionPublic(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600933 """Mark CLs public"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500934
Alex Klein1699fab2022-09-08 08:46:06 -0600935 COMMAND = "public"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500936
Alex Klein1699fab2022-09-08 08:46:06 -0600937 @staticmethod
938 def _process_one(helper, cl, opts):
939 """Use |helper| to process the single |cl|."""
940 helper.SetPrivate(cl, False, dryrun=opts.dryrun)
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500941
942
943class ActionSethashtags(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -0600944 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500945
Alex Klein1699fab2022-09-08 08:46:06 -0600946 COMMAND = "hashtags"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500947
Alex Klein1699fab2022-09-08 08:46:06 -0600948 @staticmethod
949 def init_subparser(parser):
950 """Add arguments to this action's subparser."""
951 parser.add_argument("cl", metavar="CL", help="The CL to update")
952 parser.add_argument(
953 "hashtags", nargs="+", help="The hashtags to add/remove"
954 )
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500955
Alex Klein1699fab2022-09-08 08:46:06 -0600956 @staticmethod
957 def __call__(opts):
958 """Implement the action."""
Mike Frysinger32c1d9f2023-06-22 09:07:19 -0400959 add, remove = process_add_remove_lists(opts.hashtags)
Alex Klein1699fab2022-09-08 08:46:06 -0600960 helper, cl = GetGerrit(opts, opts.cl)
961 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800962
963
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400964class ActionDelete(_ActionSimpleParallelCLs):
965 """Delete CLs"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500966
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400967 COMMAND = "delete"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500968
Alex Klein1699fab2022-09-08 08:46:06 -0600969 @staticmethod
970 def _process_one(helper, cl, opts):
971 """Use |helper| to process the single |cl|."""
Mike Frysingerf91b0f92023-04-17 16:05:27 -0400972 helper.Delete(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800973
974
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500975class ActionReviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600976 """Mark CLs as reviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500977
Alex Klein1699fab2022-09-08 08:46:06 -0600978 COMMAND = "reviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500979
Alex Klein1699fab2022-09-08 08:46:06 -0600980 @staticmethod
981 def _process_one(helper, cl, opts):
982 """Use |helper| to process the single |cl|."""
983 helper.ReviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500984
985
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500986class ActionUnreviewed(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600987 """Mark CLs as unreviewed"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500988
Alex Klein1699fab2022-09-08 08:46:06 -0600989 COMMAND = "unreviewed"
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500990
Alex Klein1699fab2022-09-08 08:46:06 -0600991 @staticmethod
992 def _process_one(helper, cl, opts):
993 """Use |helper| to process the single |cl|."""
994 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500995
996
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500997class ActionIgnore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -0600998 """Ignore CLs (suppress notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -0500999
Alex Klein1699fab2022-09-08 08:46:06 -06001000 COMMAND = "ignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001001
Alex Klein1699fab2022-09-08 08:46:06 -06001002 @staticmethod
1003 def _process_one(helper, cl, opts):
1004 """Use |helper| to process the single |cl|."""
1005 helper.IgnoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -05001006
1007
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001008class ActionUnignore(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001009 """Unignore CLs (enable notifications/dashboard/etc...)"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001010
Alex Klein1699fab2022-09-08 08:46:06 -06001011 COMMAND = "unignore"
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001012
Alex Klein1699fab2022-09-08 08:46:06 -06001013 @staticmethod
1014 def _process_one(helper, cl, opts):
1015 """Use |helper| to process the single |cl|."""
1016 helper.UnignoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger6f04dd42020-02-06 16:48:18 -05001017
1018
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001019class ActionCherryPick(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001020 """Cherry-pick CLs to branches."""
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001021
Alex Klein1699fab2022-09-08 08:46:06 -06001022 COMMAND = "cherry-pick"
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001023
Alex Klein1699fab2022-09-08 08:46:06 -06001024 @staticmethod
1025 def init_subparser(parser):
1026 """Add arguments to this action's subparser."""
1027 # Should we add an option to walk Cq-Depend and try to cherry-pick them?
1028 parser.add_argument(
1029 "--rev",
1030 "--revision",
1031 default="current",
1032 help="A specific revision or patchset",
1033 )
1034 parser.add_argument(
1035 "-m",
1036 "--msg",
1037 "--message",
1038 metavar="MESSAGE",
1039 help="Include a message",
1040 )
1041 parser.add_argument(
1042 "--branches",
1043 "--branch",
1044 "--br",
1045 action="split_extend",
1046 default=[],
1047 required=True,
1048 help="The destination branches",
1049 )
1050 parser.add_argument(
1051 "cls", nargs="+", metavar="CL", help="The CLs to cherry-pick"
1052 )
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001053
Alex Klein1699fab2022-09-08 08:46:06 -06001054 @staticmethod
1055 def __call__(opts):
1056 """Implement the action."""
Mike Frysinger16474792023-03-01 01:18:00 -05001057
Alex Klein1699fab2022-09-08 08:46:06 -06001058 # Process branches in parallel, but CLs in serial in case of CL stacks.
1059 def task(branch):
1060 for arg in opts.cls:
1061 helper, cl = GetGerrit(opts, arg)
1062 ret = helper.CherryPick(
1063 cl,
1064 branch,
1065 rev=opts.rev,
1066 msg=opts.msg,
1067 dryrun=opts.dryrun,
1068 notify=opts.notify,
1069 )
1070 logging.debug("Response: %s", ret)
1071 if opts.format is OutputFormat.RAW:
1072 print(ret["_number"])
1073 else:
1074 uri = f'https://{helper.host}/c/{ret["_number"]}'
1075 print(uri_lib.ShortenUri(uri))
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001076
Alex Klein1699fab2022-09-08 08:46:06 -06001077 _run_parallel_tasks(task, *opts.branches)
Mike Frysinger5dab15e2020-08-06 10:11:03 -04001078
1079
Mike Frysinger8037f752020-02-29 20:47:09 -05001080class ActionReview(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001081 """Review CLs with multiple settings
Mike Frysinger8037f752020-02-29 20:47:09 -05001082
Mike Frysingerd74a8bc2023-06-22 09:42:55 -04001083 The reviewers & cc options can remove people by prepending '~' or '-'.
1084 Note: If you want to move someone (reviewer->CC or CC->reviewer), you don't
1085 have to remove them first, you only need to specify the final state.
Mike Frysingere5a69832023-06-22 09:34:57 -04001086
Alex Klein4507b172023-01-13 11:39:51 -07001087 The label option supports extended/multiple syntax for easy use. The --label
1088 option may be specified multiple times (as settings are merges), and
1089 multiple labels are allowed in a single argument. Each label has the form:
Alex Klein1699fab2022-09-08 08:46:06 -06001090 <long or short name><=+-><value>
Mike Frysinger8037f752020-02-29 20:47:09 -05001091
Alex Klein1699fab2022-09-08 08:46:06 -06001092 Common arguments:
1093 Commit-Queue=0 Commit-Queue-1 Commit-Queue+2 CQ+2
1094 'V+1 CQ+2'
1095 'AS=1 V=1'
1096 """
Mike Frysinger8037f752020-02-29 20:47:09 -05001097
Alex Klein1699fab2022-09-08 08:46:06 -06001098 COMMAND = "review"
Mike Frysinger8037f752020-02-29 20:47:09 -05001099
Alex Klein1699fab2022-09-08 08:46:06 -06001100 class _SetLabel(argparse.Action):
1101 """Argparse action for setting labels."""
Mike Frysinger8037f752020-02-29 20:47:09 -05001102
Alex Klein1699fab2022-09-08 08:46:06 -06001103 LABEL_MAP = {
1104 "AS": "Auto-Submit",
1105 "CQ": "Commit-Queue",
1106 "CR": "Code-Review",
1107 "V": "Verified",
1108 }
Mike Frysinger8037f752020-02-29 20:47:09 -05001109
Alex Klein1699fab2022-09-08 08:46:06 -06001110 def __call__(self, parser, namespace, values, option_string=None):
1111 labels = getattr(namespace, self.dest)
1112 for request in values.split():
1113 if "=" in request:
1114 # Handle Verified=1 form.
1115 short, value = request.split("=", 1)
1116 elif "+" in request:
1117 # Handle Verified+1 form.
1118 short, value = request.split("+", 1)
1119 elif "-" in request:
1120 # Handle Verified-1 form.
1121 short, value = request.split("-", 1)
1122 value = "-%s" % (value,)
1123 else:
1124 parser.error(
1125 'Invalid label setting "%s". Must be Commit-Queue=1 or '
1126 "CQ+1 or CR-1." % (request,)
1127 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001128
Alex Klein1699fab2022-09-08 08:46:06 -06001129 # Convert possible short label names like "V" to "Verified".
1130 label = self.LABEL_MAP.get(short)
1131 if not label:
1132 label = short
Mike Frysinger8037f752020-02-29 20:47:09 -05001133
Alex Klein1699fab2022-09-08 08:46:06 -06001134 # We allow existing label requests to be overridden.
1135 labels[label] = value
Mike Frysinger8037f752020-02-29 20:47:09 -05001136
Alex Klein1699fab2022-09-08 08:46:06 -06001137 @classmethod
1138 def init_subparser(cls, parser):
1139 """Add arguments to this action's subparser."""
1140 parser.add_argument(
1141 "-m",
1142 "--msg",
1143 "--message",
1144 metavar="MESSAGE",
1145 help="Include a message",
1146 )
1147 parser.add_argument(
1148 "-l",
1149 "--label",
1150 dest="labels",
1151 action=cls._SetLabel,
1152 default={},
1153 help="Set a label with a value",
1154 )
1155 parser.add_argument(
1156 "--ready",
1157 default=None,
1158 action="store_true",
1159 help="Set CL status to ready-for-review",
1160 )
1161 parser.add_argument(
1162 "--wip",
1163 default=None,
1164 action="store_true",
1165 help="Set CL status to WIP",
1166 )
1167 parser.add_argument(
1168 "--reviewers",
1169 "--re",
1170 action="append",
1171 default=[],
Mike Frysingere5a69832023-06-22 09:34:57 -04001172 help="Reviewers to add/remove",
Alex Klein1699fab2022-09-08 08:46:06 -06001173 )
1174 parser.add_argument(
Mike Frysingere5a69832023-06-22 09:34:57 -04001175 "--cc",
1176 action="append",
1177 default=[],
1178 help="People to add/remove in CC",
Alex Klein1699fab2022-09-08 08:46:06 -06001179 )
1180 _ActionSimpleParallelCLs.init_subparser(parser)
Mike Frysinger8037f752020-02-29 20:47:09 -05001181
Alex Klein1699fab2022-09-08 08:46:06 -06001182 @staticmethod
1183 def _process_one(helper, cl, opts):
1184 """Use |helper| to process the single |cl|."""
Mike Frysingere5a69832023-06-22 09:34:57 -04001185 add_reviewers, remove_reviewers = process_add_remove_lists(
1186 opts.reviewers, f"^{constants.EMAIL_REGEX}$"
1187 )
1188 add_cc, remove_cc = process_add_remove_lists(
1189 opts.cc, f"^{constants.EMAIL_REGEX}$"
1190 )
1191
1192 # Gerrit allows people to only be in one state: CC or Reviewer. If a
1193 # person is in CC and you want to move them to reviewer, you can't
1194 # remove them from CC and add to reviewer, you have to change their
1195 # state. Help users who do `--cc ~u@c --re u@c` by filtering out all
1196 # the remove requests if there is an add request too. This doesn't
1197 # quite respect all the possible CLI option orders, but it's probably
1198 # good enough for now in practice. For example, mixing of CC & reviewer
1199 # and adds & removes gets complicated.
1200 for add in add_cc:
1201 if add in remove_reviewers:
1202 remove_reviewers.remove(add)
1203 for add in add_reviewers:
1204 if add in remove_cc:
1205 remove_cc.remove(add)
1206
Alex Klein1699fab2022-09-08 08:46:06 -06001207 helper.SetReview(
1208 cl,
1209 msg=opts.msg,
1210 labels=opts.labels,
1211 dryrun=opts.dryrun,
1212 notify=opts.notify,
Mike Frysingere5a69832023-06-22 09:34:57 -04001213 reviewers=add_reviewers,
1214 cc=add_cc,
1215 remove_reviewers=remove_reviewers | remove_cc,
Alex Klein1699fab2022-09-08 08:46:06 -06001216 ready=opts.ready,
1217 wip=opts.wip,
1218 )
Mike Frysinger8037f752020-02-29 20:47:09 -05001219
1220
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001221class ActionAccount(_ActionSimpleParallelCLs):
Alex Klein1699fab2022-09-08 08:46:06 -06001222 """Get user account information"""
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001223
Alex Klein1699fab2022-09-08 08:46:06 -06001224 COMMAND = "account"
1225 USE_PAGER = True
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001226
Alex Klein1699fab2022-09-08 08:46:06 -06001227 @staticmethod
1228 def init_subparser(parser):
1229 """Add arguments to this action's subparser."""
1230 parser.add_argument(
1231 "accounts",
1232 nargs="*",
1233 default=["self"],
1234 help="The accounts to query",
1235 )
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001236
Alex Klein1699fab2022-09-08 08:46:06 -06001237 @classmethod
1238 def __call__(cls, opts):
1239 """Implement the action."""
1240 helper, _ = GetGerrit(opts)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001241
Alex Klein1699fab2022-09-08 08:46:06 -06001242 def print_one(header, data):
1243 print(f"### {header}")
1244 compact = opts.format is OutputFormat.JSON
1245 print(pformat.json(data, compact=compact).rstrip())
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001246
Alex Klein1699fab2022-09-08 08:46:06 -06001247 def task(arg):
1248 detail = gob_util.FetchUrlJson(
1249 helper.host, f"accounts/{arg}/detail"
1250 )
1251 if not detail:
1252 print(f"{arg}: account not found")
1253 else:
1254 print_one("detail", detail)
1255 for field in (
Mike Frysinger03c75072023-03-10 17:25:21 -05001256 "external.ids",
Alex Klein1699fab2022-09-08 08:46:06 -06001257 "groups",
1258 "capabilities",
1259 "preferences",
1260 "sshkeys",
1261 "gpgkeys",
1262 ):
1263 data = gob_util.FetchUrlJson(
1264 helper.host, f"accounts/{arg}/{field}"
1265 )
1266 print_one(field, data)
Mike Frysinger7f2018d2021-02-04 00:10:58 -05001267
Alex Klein1699fab2022-09-08 08:46:06 -06001268 _run_parallel_tasks(task, *opts.accounts)
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -08001269
1270
Mike Frysinger2295d792021-03-08 15:55:23 -05001271class ActionConfig(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001272 """Manage the gerrit tool's own config file
Mike Frysinger2295d792021-03-08 15:55:23 -05001273
Alex Klein1699fab2022-09-08 08:46:06 -06001274 Gerrit may be customized via ~/.config/chromite/gerrit.cfg.
1275 It is an ini file like ~/.gitconfig. See `man git-config` for basic format.
Mike Frysinger2295d792021-03-08 15:55:23 -05001276
Alex Klein1699fab2022-09-08 08:46:06 -06001277 # Set up subcommand aliases.
1278 [alias]
1279 common-search = search 'is:open project:something/i/care/about'
1280 """
Mike Frysinger2295d792021-03-08 15:55:23 -05001281
Alex Klein1699fab2022-09-08 08:46:06 -06001282 COMMAND = "config"
Mike Frysinger2295d792021-03-08 15:55:23 -05001283
Alex Klein1699fab2022-09-08 08:46:06 -06001284 @staticmethod
1285 def __call__(opts):
1286 """Implement the action."""
Alex Klein4507b172023-01-13 11:39:51 -07001287 # For now, this is a place holder for raising visibility for the config
1288 # file and its associated help text documentation.
Alex Klein1699fab2022-09-08 08:46:06 -06001289 opts.parser.parse_args(["config", "--help"])
Mike Frysinger2295d792021-03-08 15:55:23 -05001290
1291
Mike Frysingere5450602021-03-08 15:34:17 -05001292class ActionHelp(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001293 """An alias to --help for CLI symmetry"""
Mike Frysingere5450602021-03-08 15:34:17 -05001294
Alex Klein1699fab2022-09-08 08:46:06 -06001295 COMMAND = "help"
1296 USE_PAGER = True
Mike Frysingere5450602021-03-08 15:34:17 -05001297
Alex Klein1699fab2022-09-08 08:46:06 -06001298 @staticmethod
1299 def init_subparser(parser):
1300 """Add arguments to this action's subparser."""
1301 parser.add_argument(
1302 "command", nargs="?", help="The command to display."
1303 )
Mike Frysingere5450602021-03-08 15:34:17 -05001304
Alex Klein1699fab2022-09-08 08:46:06 -06001305 @staticmethod
1306 def __call__(opts):
1307 """Implement the action."""
1308 # Show global help.
1309 if not opts.command:
1310 opts.parser.print_help()
1311 return
Mike Frysingere5450602021-03-08 15:34:17 -05001312
Alex Klein1699fab2022-09-08 08:46:06 -06001313 opts.parser.parse_args([opts.command, "--help"])
Mike Frysingere5450602021-03-08 15:34:17 -05001314
1315
Mike Frysinger484e2f82020-03-20 01:41:10 -04001316class ActionHelpAll(UserAction):
Alex Klein1699fab2022-09-08 08:46:06 -06001317 """Show all actions help output at once."""
Mike Frysinger484e2f82020-03-20 01:41:10 -04001318
Alex Klein1699fab2022-09-08 08:46:06 -06001319 COMMAND = "help-all"
1320 USE_PAGER = True
Mike Frysinger484e2f82020-03-20 01:41:10 -04001321
Alex Klein1699fab2022-09-08 08:46:06 -06001322 @staticmethod
1323 def __call__(opts):
1324 """Implement the action."""
1325 first = True
1326 for action in _GetActions():
1327 if first:
1328 first = False
1329 else:
1330 print("\n\n")
Mike Frysinger484e2f82020-03-20 01:41:10 -04001331
Alex Klein1699fab2022-09-08 08:46:06 -06001332 try:
1333 opts.parser.parse_args([action, "--help"])
1334 except SystemExit:
1335 pass
Mike Frysinger484e2f82020-03-20 01:41:10 -04001336
1337
Mike Frysinger65fc8632020-02-06 18:11:12 -05001338@memoize.Memoize
1339def _GetActions():
Alex Klein1699fab2022-09-08 08:46:06 -06001340 """Get all the possible actions we support.
Mike Frysinger65fc8632020-02-06 18:11:12 -05001341
Alex Klein1699fab2022-09-08 08:46:06 -06001342 Returns:
Trent Apted66736d82023-05-25 10:38:28 +10001343 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
1344 function that implements that command (e.g. UserActFoo).
Alex Klein1699fab2022-09-08 08:46:06 -06001345 """
1346 VALID_NAME = re.compile(r"^[a-z][a-z-]*[a-z]$")
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001347
Alex Klein1699fab2022-09-08 08:46:06 -06001348 actions = {}
1349 for cls in globals().values():
1350 if (
1351 not inspect.isclass(cls)
1352 or not issubclass(cls, UserAction)
1353 or not getattr(cls, "COMMAND", None)
1354 ):
1355 continue
Mike Frysinger65fc8632020-02-06 18:11:12 -05001356
Alex Klein1699fab2022-09-08 08:46:06 -06001357 # Sanity check names for devs adding new commands. Should be quick.
1358 cmd = cls.COMMAND
1359 assert VALID_NAME.match(cmd), '"%s" must match [a-z-]+' % (cmd,)
1360 assert cmd not in actions, 'multiple "%s" commands found' % (cmd,)
Mike Frysinger65fc8632020-02-06 18:11:12 -05001361
Alex Klein1699fab2022-09-08 08:46:06 -06001362 actions[cmd] = cls
Mike Frysinger65fc8632020-02-06 18:11:12 -05001363
Alex Klein1699fab2022-09-08 08:46:06 -06001364 return collections.OrderedDict(sorted(actions.items()))
Mike Frysinger65fc8632020-02-06 18:11:12 -05001365
1366
Harry Cutts26076b32019-02-26 15:01:29 -08001367def _GetActionUsages():
Alex Klein1699fab2022-09-08 08:46:06 -06001368 """Formats a one-line usage and doc message for each action."""
1369 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -08001370
Alex Klein1699fab2022-09-08 08:46:06 -06001371 cmds = list(actions.keys())
1372 functions = list(actions.values())
1373 usages = [getattr(x, "usage", "") for x in functions]
1374 docs = [x.__doc__.splitlines()[0] for x in functions]
Harry Cutts26076b32019-02-26 15:01:29 -08001375
Alex Klein1699fab2022-09-08 08:46:06 -06001376 cmd_indent = len(max(cmds, key=len))
1377 usage_indent = len(max(usages, key=len))
1378 return "\n".join(
1379 " %-*s %-*s : %s" % (cmd_indent, cmd, usage_indent, usage, doc)
1380 for cmd, usage, doc in zip(cmds, usages, docs)
1381 )
Harry Cutts26076b32019-02-26 15:01:29 -08001382
1383
Mike Frysinger2295d792021-03-08 15:55:23 -05001384def _AddCommonOptions(parser, subparser):
Alex Klein1699fab2022-09-08 08:46:06 -06001385 """Add options that should work before & after the subcommand.
Mike Frysinger2295d792021-03-08 15:55:23 -05001386
Alex Klein1699fab2022-09-08 08:46:06 -06001387 Make it easy to do `gerrit --dry-run foo` and `gerrit foo --dry-run`.
1388 """
1389 parser.add_common_argument_to_group(
1390 subparser,
1391 "--ne",
1392 "--no-emails",
1393 dest="notify",
1394 default="ALL",
1395 action="store_const",
1396 const="NONE",
1397 help="Do not send e-mail notifications",
1398 )
1399 parser.add_common_argument_to_group(
1400 subparser,
1401 "-n",
1402 "--dry-run",
1403 dest="dryrun",
1404 default=False,
1405 action="store_true",
1406 help="Show what would be done, but do not make changes",
1407 )
Mike Frysinger2295d792021-03-08 15:55:23 -05001408
1409
1410def GetBaseParser() -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001411 """Returns the common parser (i.e. no subparsers added)."""
1412 description = """\
Mike Frysinger13f23a42013-05-13 17:32:01 -04001413There is no support for doing line-by-line code review via the command line.
1414This helps you manage various bits and CL status.
1415
Mike Frysingera1db2c42014-06-15 00:42:48 -07001416For general Gerrit documentation, see:
1417 https://gerrit-review.googlesource.com/Documentation/
1418The Searching Changes page covers the search query syntax:
1419 https://gerrit-review.googlesource.com/Documentation/user-search.html
1420
Mike Frysinger13f23a42013-05-13 17:32:01 -04001421Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001422 $ gerrit todo # List all the CLs that await your review.
1423 $ gerrit mine # List all of your open CLs.
1424 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
1425 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
1426 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -08001427 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
142828123.
1429 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
1430CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -07001431Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -05001432 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
1433with Commit-Queue=1.
1434 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
1435CLs with Commit-Queue=1.
Mike Frysingerd7f10792021-03-08 13:11:38 -05001436 $ gerrit --json search 'attention:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -04001437
Harry Cutts26076b32019-02-26 15:01:29 -08001438Actions:
1439"""
Alex Klein1699fab2022-09-08 08:46:06 -06001440 description += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -04001441
Alex Klein1699fab2022-09-08 08:46:06 -06001442 site_params = config_lib.GetSiteParams()
1443 parser = commandline.ArgumentParser(
1444 description=description,
1445 default_log_level="notice",
1446 epilog="For subcommand help, use `gerrit help <command>`.",
1447 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001448
Alex Klein1699fab2022-09-08 08:46:06 -06001449 group = parser.add_argument_group("Server options")
1450 group.add_argument(
1451 "-i",
1452 "--internal",
1453 dest="gob",
1454 action="store_const",
1455 default=site_params.EXTERNAL_GOB_INSTANCE,
1456 const=site_params.INTERNAL_GOB_INSTANCE,
1457 help="Query internal Chrome Gerrit instance",
1458 )
1459 group.add_argument(
1460 "-g",
1461 "--gob",
1462 default=site_params.EXTERNAL_GOB_INSTANCE,
Trent Apted66736d82023-05-25 10:38:28 +10001463 help="Gerrit (on borg) instance to query (default: %(default)s)",
Alex Klein1699fab2022-09-08 08:46:06 -06001464 )
Mike Frysinger8674a112021-02-09 14:44:17 -05001465
Alex Klein1699fab2022-09-08 08:46:06 -06001466 group = parser.add_argument_group("CL options")
1467 _AddCommonOptions(parser, group)
Mike Frysinger8674a112021-02-09 14:44:17 -05001468
Alex Klein1699fab2022-09-08 08:46:06 -06001469 group = parser.add_mutually_exclusive_group()
1470 parser.set_defaults(format=OutputFormat.AUTO)
1471 group.add_argument(
1472 "--format",
1473 action="enum",
1474 enum=OutputFormat,
1475 help="Output format to use.",
1476 )
1477 group.add_argument(
1478 "--raw",
1479 action="store_const",
1480 dest="format",
1481 const=OutputFormat.RAW,
1482 help="Alias for --format=raw.",
1483 )
1484 group.add_argument(
1485 "--json",
1486 action="store_const",
1487 dest="format",
1488 const=OutputFormat.JSON,
1489 help="Alias for --format=json.",
1490 )
Jack Rosenthal95aac172022-06-30 15:35:07 -06001491
Alex Klein1699fab2022-09-08 08:46:06 -06001492 group = parser.add_mutually_exclusive_group()
1493 group.add_argument(
1494 "--pager",
1495 action="store_true",
1496 default=sys.stdout.isatty(),
1497 help="Enable pager.",
1498 )
1499 group.add_argument(
1500 "--no-pager", action="store_false", dest="pager", help="Disable pager."
1501 )
1502 return parser
Mike Frysinger2295d792021-03-08 15:55:23 -05001503
1504
Alex Klein1699fab2022-09-08 08:46:06 -06001505def GetParser(
1506 parser: commandline.ArgumentParser = None,
Mike Frysinger16474792023-03-01 01:18:00 -05001507) -> commandline.ArgumentParser:
Alex Klein1699fab2022-09-08 08:46:06 -06001508 """Returns the full parser to use for this module."""
1509 if parser is None:
1510 parser = GetBaseParser()
Mike Frysinger2295d792021-03-08 15:55:23 -05001511
Alex Klein1699fab2022-09-08 08:46:06 -06001512 actions = _GetActions()
Mike Frysingerc7796cf2020-02-06 23:55:15 -05001513
Alex Klein1699fab2022-09-08 08:46:06 -06001514 # Subparsers are required by default under Python 2. Python 3 changed to
1515 # not required, but didn't include a required option until 3.7. Setting
1516 # the required member works in all versions (and setting dest name).
1517 subparsers = parser.add_subparsers(dest="action")
1518 subparsers.required = True
1519 for cmd, cls in actions.items():
1520 # Format the full docstring by removing the file level indentation.
1521 description = re.sub(r"^ ", "", cls.__doc__, flags=re.M)
1522 subparser = subparsers.add_parser(cmd, description=description)
1523 _AddCommonOptions(parser, subparser)
1524 cls.init_subparser(subparser)
Mike Frysinger108eda22018-06-06 18:45:12 -04001525
Alex Klein1699fab2022-09-08 08:46:06 -06001526 return parser
Mike Frysinger108eda22018-06-06 18:45:12 -04001527
1528
Jack Rosenthal95aac172022-06-30 15:35:07 -06001529def start_pager():
Alex Klein1699fab2022-09-08 08:46:06 -06001530 """Re-spawn ourselves attached to a pager."""
1531 pager = os.environ.get("PAGER", "less")
1532 os.environ.setdefault("LESS", "FRX")
Jack Rosenthal95aac172022-06-30 15:35:07 -06001533 with subprocess.Popen(
Alex Klein1699fab2022-09-08 08:46:06 -06001534 # sys.argv can have some edge cases: we may not necessarily use
1535 # sys.executable if the script is executed as "python path/to/script".
1536 # If we upgrade to Python 3.10+, this should be changed to sys.orig_argv
1537 # for full accuracy.
1538 sys.argv,
1539 stdout=subprocess.PIPE,
1540 stderr=subprocess.STDOUT,
1541 env={"GERRIT_RESPAWN_FOR_PAGER": "1", **os.environ},
1542 ) as gerrit_proc:
1543 with subprocess.Popen(
1544 pager,
1545 shell=True,
1546 stdin=gerrit_proc.stdout,
1547 ) as pager_proc:
1548 # Send SIGINT to just the gerrit process, not the pager too.
1549 def _sighandler(signum, _frame):
1550 gerrit_proc.send_signal(signum)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001551
Alex Klein1699fab2022-09-08 08:46:06 -06001552 signal.signal(signal.SIGINT, _sighandler)
Jack Rosenthal95aac172022-06-30 15:35:07 -06001553
Alex Klein1699fab2022-09-08 08:46:06 -06001554 pager_proc.communicate()
1555 # If the pager exits, and the gerrit process is still running, we
1556 # must terminate it.
1557 if gerrit_proc.poll() is None:
1558 gerrit_proc.terminate()
1559 sys.exit(gerrit_proc.wait())
Jack Rosenthal95aac172022-06-30 15:35:07 -06001560
1561
Mike Frysinger108eda22018-06-06 18:45:12 -04001562def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -06001563 base_parser = GetBaseParser()
1564 opts, subargs = base_parser.parse_known_args(argv)
Mike Frysinger2295d792021-03-08 15:55:23 -05001565
Alex Klein1699fab2022-09-08 08:46:06 -06001566 config = Config()
1567 if subargs:
Alex Klein4507b172023-01-13 11:39:51 -07001568 # If the action is an alias to an expanded value, we need to mutate the
1569 # argv and reparse things.
Alex Klein1699fab2022-09-08 08:46:06 -06001570 action = config.expand_alias(subargs[0])
1571 if action != subargs[0]:
1572 pos = argv.index(subargs[0])
1573 argv = argv[:pos] + action + argv[pos + 1 :]
Mike Frysinger2295d792021-03-08 15:55:23 -05001574
Alex Klein1699fab2022-09-08 08:46:06 -06001575 parser = GetParser(parser=base_parser)
1576 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -04001577
Alex Klein1699fab2022-09-08 08:46:06 -06001578 # If we're running as a re-spawn for the pager, from this point on
1579 # we'll pretend we're attached to a TTY. This will give us colored
1580 # output when requested.
1581 if os.environ.pop("GERRIT_RESPAWN_FOR_PAGER", None) is not None:
1582 opts.pager = False
1583 sys.stdout.isatty = lambda: True
Jack Rosenthal95aac172022-06-30 15:35:07 -06001584
Alex Klein1699fab2022-09-08 08:46:06 -06001585 # In case the action wants to throw a parser error.
1586 opts.parser = parser
Mike Frysinger484e2f82020-03-20 01:41:10 -04001587
Alex Klein1699fab2022-09-08 08:46:06 -06001588 # A cache of gerrit helpers we'll load on demand.
1589 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -08001590
Alex Klein1699fab2022-09-08 08:46:06 -06001591 if opts.format is OutputFormat.AUTO:
1592 if sys.stdout.isatty():
1593 opts.format = OutputFormat.PRETTY
1594 else:
1595 opts.format = OutputFormat.RAW
Jack Rosenthale3a92672022-06-29 14:54:48 -06001596
Alex Klein1699fab2022-09-08 08:46:06 -06001597 opts.Freeze()
Mike Frysinger88f27292014-06-17 09:40:45 -07001598
Alex Klein1699fab2022-09-08 08:46:06 -06001599 # pylint: disable=global-statement
1600 global COLOR
1601 COLOR = terminal.Color(enabled=opts.color)
Mike Frysinger031ad0b2013-05-14 18:15:34 -04001602
Alex Klein1699fab2022-09-08 08:46:06 -06001603 # Now look up the requested user action and run it.
1604 actions = _GetActions()
1605 action_class = actions[opts.action]
1606 if action_class.USE_PAGER and opts.pager:
1607 start_pager()
1608 obj = action_class()
1609 try:
1610 obj(opts)
1611 except (
1612 cros_build_lib.RunCommandError,
1613 gerrit.GerritException,
1614 gob_util.GOBError,
1615 ) as e:
1616 cros_build_lib.Die(e)