blob: cfe83213fe04f60a3c03de4aee3ce9b63f29e178 [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Mike Frysinger13f23a42013-05-13 17:32:01 -04002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysinger08737512014-02-07 22:58:26 -05006"""A command line interface to Gerrit-on-borg instances.
Mike Frysinger13f23a42013-05-13 17:32:01 -04007
8Internal Note:
9To expose a function directly to the command line interface, name your function
10with the prefix "UserAct".
11"""
12
Mike Frysinger31ff6f92014-02-08 04:33:03 -050013from __future__ import print_function
14
Mike Frysinger65fc8632020-02-06 18:11:12 -050015import collections
Mike Frysinger13f23a42013-05-13 17:32:01 -040016import inspect
Mike Frysinger87c74ce2017-04-04 16:12:31 -040017import json
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070018import re
Mike Frysinger87c74ce2017-04-04 16:12:31 -040019import sys
Mike Frysinger13f23a42013-05-13 17:32:01 -040020
Aviv Keshetb7519e12016-10-04 00:50:00 -070021from chromite.lib import config_lib
22from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040023from chromite.lib import commandline
24from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070025from chromite.lib import cros_logging as logging
Mike Frysinger13f23a42013-05-13 17:32:01 -040026from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050027from chromite.lib import gob_util
Mike Frysinger254f33f2019-12-11 13:54:29 -050028from chromite.lib import parallel
Mike Frysinger13f23a42013-05-13 17:32:01 -040029from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040030from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060031from chromite.utils import memoize
Mike Frysinger13f23a42013-05-13 17:32:01 -040032
33
Mike Frysinger1c76d4c2020-02-08 23:35:29 -050034assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
35
36
Mike Frysinger108eda22018-06-06 18:45:12 -040037# Locate actions that are exposed to the user. All functions that start
38# with "UserAct" are fair game.
39ACTION_PREFIX = 'UserAct'
40
41
Mike Frysinger254f33f2019-12-11 13:54:29 -050042# How many connections we'll use in parallel. We don't want this to be too high
43# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
44# seems to be good enough for users.
45CONNECTION_LIMIT = 10
46
47
Mike Frysinger031ad0b2013-05-14 18:15:34 -040048COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040049
50# Map the internal names to the ones we normally show on the web ui.
51GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080052 'COMR': ['CQ', 'Commit Queue ',],
53 'CRVW': ['CR', 'Code Review ',],
54 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080055 'VRIF': ['V ', 'Verified ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060056 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040057}
58
59# Order is important -- matches the web ui. This also controls the short
60# entries that we summarize in non-verbose mode.
61GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
62
Mike Frysinger4aea5dc2019-07-17 13:39:56 -040063# Shorter strings for CL status messages.
64GERRIT_SUMMARY_MAP = {
65 'ABANDONED': 'ABD',
66 'MERGED': 'MRG',
67 'NEW': 'NEW',
68 'WIP': 'WIP',
69}
70
Mike Frysinger13f23a42013-05-13 17:32:01 -040071
72def red(s):
73 return COLOR.Color(terminal.Color.RED, s)
74
75
76def green(s):
77 return COLOR.Color(terminal.Color.GREEN, s)
78
79
80def blue(s):
81 return COLOR.Color(terminal.Color.BLUE, s)
82
83
Mike Frysinger254f33f2019-12-11 13:54:29 -050084def _run_parallel_tasks(task, *args):
85 """Small wrapper around BackgroundTaskRunner to enforce job count."""
86 with parallel.BackgroundTaskRunner(task, processes=CONNECTION_LIMIT) as q:
87 for arg in args:
88 q.put([arg])
89
90
Mike Frysinger13f23a42013-05-13 17:32:01 -040091def limits(cls):
92 """Given a dict of fields, calculate the longest string lengths
93
94 This allows you to easily format the output of many results so that the
95 various cols all line up correctly.
96 """
97 lims = {}
98 for cl in cls:
99 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -0400100 # Use %s rather than str() to avoid codec issues.
101 # We also do this so we can format integers.
102 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400103 return lims
104
105
Mike Frysinger88f27292014-06-17 09:40:45 -0700106# TODO: This func really needs to be merged into the core gerrit logic.
107def GetGerrit(opts, cl=None):
108 """Auto pick the right gerrit instance based on the |cl|
109
110 Args:
111 opts: The general options object.
112 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
113
114 Returns:
115 A tuple of a gerrit object and a sanitized CL #.
116 """
117 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700118 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600119 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600120 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600121 if cl.startswith('*'):
122 cl = cl[1:]
123 else:
124 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700125 elif ':' in cl:
126 gob, cl = cl.split(':', 1)
127
128 if not gob in opts.gerrit:
129 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
130
131 return (opts.gerrit[gob], cl)
132
133
Mike Frysinger13f23a42013-05-13 17:32:01 -0400134def GetApprovalSummary(_opts, cls):
135 """Return a dict of the most important approvals"""
136 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700137 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
138 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
139 if not cats:
140 logging.warning('unknown gerrit approval type: %s', approver['type'])
141 continue
142 cat = cats[0].strip()
143 val = int(approver['value'])
144 if not cat in approvs:
145 # Ignore the extended categories in the summary view.
146 continue
147 elif approvs[cat] == '':
148 approvs[cat] = val
149 elif val < 0:
150 approvs[cat] = min(approvs[cat], val)
151 else:
152 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400153 return approvs
154
155
Mike Frysingera1b4b272017-04-05 16:11:00 -0400156def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400157 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400158 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400159 lims = {'url': 0, 'project': 0}
160
161 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400162
163 if opts.verbose:
164 status += '%s ' % (cl['status'],)
165 else:
166 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
167
Mike Frysinger13f23a42013-05-13 17:32:01 -0400168 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400169 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400170 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400171 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400172 functor = lambda x: x
173 elif approvs[cat] < 0:
174 functor = red
175 else:
176 functor = green
177 status += functor('%s:%2s ' % (cat, approvs[cat]))
178
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400179 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
180 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400181
182 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400183 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400184 functor = red if int(approver['value']) < 0 else green
185 n = functor('%2s' % approver['value'])
186 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
187 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500188 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400189
190
Mike Frysingera1b4b272017-04-05 16:11:00 -0400191def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400192 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400193 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600194 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400195 pfx = ''
196 # Special case internal Chrome GoB as that is what most devs use.
197 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600198 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
199 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400200 for cl in cls:
201 print('%s%s' % (pfx, cl['number']))
202
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400203 elif opts.json:
204 json.dump(cls, sys.stdout)
205
Mike Frysingera1b4b272017-04-05 16:11:00 -0400206 else:
207 if lims is None:
208 lims = limits(cls)
209
210 for cl in cls:
211 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
212
213
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400214def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700215 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800216 if opts.branch is not None:
217 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800218 if opts.project is not None:
219 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800220 if opts.topic is not None:
221 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800222
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400223 if helper is None:
224 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700225 return helper.Query(query, raw=raw, bypass_cache=False)
226
227
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400228def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700229 """Query gerrit and filter/clean up the results"""
230 ret = []
231
Mike Frysinger2cd56022017-01-12 20:56:27 -0500232 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400233 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400234 # Gerrit likes to return a stats record too.
235 if not 'project' in cl:
236 continue
237
238 # Strip off common leading names since the result is still
239 # unique over the whole tree.
240 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400241 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
242 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400243 if cl['project'].startswith('%s/' % pfx):
244 cl['project'] = cl['project'][len(pfx) + 1:]
245
Mike Frysinger479f1192017-09-14 22:36:30 -0400246 cl['url'] = uri_lib.ShortenUri(cl['url'])
247
Mike Frysinger13f23a42013-05-13 17:32:01 -0400248 ret.append(cl)
249
Mike Frysingerb62313a2017-06-30 16:38:58 -0400250 if opts.sort == 'unsorted':
251 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700252 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400253 key = lambda x: int(x[opts.sort])
254 else:
255 key = lambda x: x[opts.sort]
256 return sorted(ret, key=key)
257
258
Mike Frysinger13f23a42013-05-13 17:32:01 -0400259def UserActTodo(opts):
260 """List CLs needing your review"""
Mike Frysinger87690552018-12-30 22:56:06 -0500261 cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
Mike Frysingered3d7ea2017-07-10 13:14:02 -0400262 'label:Code-Review=0,user=self '
263 'NOT label:Verified<0'))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400264 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400265
266
Mike Frysingera1db2c42014-06-15 00:42:48 -0700267def UserActSearch(opts, query):
Harry Cutts26076b32019-02-26 15:01:29 -0800268 """List CLs matching the search query"""
Mike Frysingera1db2c42014-06-15 00:42:48 -0700269 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400270 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800271UserActSearch.usage = '<query>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400272
273
Mike Frysingera1db2c42014-06-15 00:42:48 -0700274def UserActMine(opts):
275 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700276 if opts.draft:
277 rule = 'is:draft'
278 else:
279 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500280 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700281
282
Paul Hobbs89765232015-06-24 14:07:49 -0700283def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
284 """Runs breadth first search starting from the nodes in |to_visit|
285
286 Args:
287 to_visit: the starting nodes
288 children: a function which takes a node and returns the nodes adjacent to it
289 visited_key: a function for deduplicating node visits. Defaults to the
290 identity function (lambda x: x)
291
292 Returns:
293 A list of nodes which are reachable from any node in |to_visit| by calling
294 |children| any number of times.
295 """
296 to_visit = list(to_visit)
Mike Frysinger66ce4132019-07-17 22:52:52 -0400297 seen = set(visited_key(x) for x in to_visit)
Paul Hobbs89765232015-06-24 14:07:49 -0700298 for node in to_visit:
299 for child in children(node):
300 key = visited_key(child)
301 if key not in seen:
302 seen.add(key)
303 to_visit.append(child)
304 return to_visit
305
306
307def UserActDeps(opts, query):
308 """List CLs matching a query, and all transitive dependencies of those CLs"""
309 cls = _Query(opts, query, raw=False)
310
Mike Frysinger10666292018-07-12 01:03:38 -0400311 @memoize.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400312 def _QueryChange(cl, helper=None):
313 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700314
Mike Frysinger5726da92017-09-20 22:14:25 -0400315 def _ProcessDeps(cl, deps, required):
316 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700317 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400318 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400319 if not dep.remote in opts.gerrit:
320 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
321 remote=dep.remote, print_cmd=opts.debug)
322 helper = opts.gerrit[dep.remote]
323
Paul Hobbs89765232015-06-24 14:07:49 -0700324 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400325 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
326
327 # Handle empty results. If we found a commit that was pushed directly
328 # (e.g. a bot commit), then gerrit won't know about it.
329 if not changes:
330 if required:
331 logging.error('CL %s depends on %s which cannot be found',
332 cl, dep.ToGerritQueryText())
333 continue
334
335 # Our query might have matched more than one result. This can come up
336 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
337 # across multiple repos/branches. We blindly check all of them in the
338 # hopes that all open ones are what the user wants, but then again the
339 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
340 if len(changes) > 1:
341 logging.warning('CL %s has an ambiguous CQ dependency %s',
342 cl, dep.ToGerritQueryText())
343 for change in changes:
344 if change.status == 'NEW':
345 yield change
346
347 def _Children(cl):
348 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
349 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
350 yield change
351 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
352 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700353
354 transitives = _BreadthFirstSearch(
355 cls, _Children,
356 visited_key=lambda cl: cl.gerrit_number)
357
358 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400359 PrintCls(opts, transitives_raw)
Harry Cutts26076b32019-02-26 15:01:29 -0800360UserActDeps.usage = '<query>'
Paul Hobbs89765232015-06-24 14:07:49 -0700361
362
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700363def UserActInspect(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800364 """Show the details of one or more CLs"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400365 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700366 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400367 helper, cl = GetGerrit(opts, arg)
368 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
369 if change:
370 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700371 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400372 logging.warning('no results found for CL %s', arg)
373 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800374UserActInspect.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400375
376
Mike Frysinger48b5e012020-02-06 17:04:12 -0500377def UserActLabel_as(opts, *args):
378 """Change the Auto-Submit label"""
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600379 num = args[-1]
Mike Frysinger254f33f2019-12-11 13:54:29 -0500380 def task(arg):
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600381 helper, cl = GetGerrit(opts, arg)
382 helper.SetReview(cl, labels={'Auto-Submit': num},
383 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500384 _run_parallel_tasks(task, *args[:-1])
Mike Frysinger48b5e012020-02-06 17:04:12 -0500385UserActLabel_as.arg_min = 2
386UserActLabel_as.usage = '<CLs...> <0|1>'
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600387
388
Mike Frysinger48b5e012020-02-06 17:04:12 -0500389def UserActLabel_cr(opts, *args):
390 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700391 num = args[-1]
Mike Frysinger254f33f2019-12-11 13:54:29 -0500392 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700393 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800394 helper.SetReview(cl, labels={'Code-Review': num},
395 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500396 _run_parallel_tasks(task, *args[:-1])
Mike Frysinger48b5e012020-02-06 17:04:12 -0500397UserActLabel_cr.arg_min = 2
398UserActLabel_cr.usage = '<CLs...> <-2|-1|0|1|2>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400399
400
Mike Frysinger48b5e012020-02-06 17:04:12 -0500401def UserActLabel_v(opts, *args):
402 """Change the Verified label"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700403 num = args[-1]
Mike Frysinger254f33f2019-12-11 13:54:29 -0500404 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700405 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800406 helper.SetReview(cl, labels={'Verified': num},
407 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500408 _run_parallel_tasks(task, *args[:-1])
Mike Frysinger48b5e012020-02-06 17:04:12 -0500409UserActLabel_v.arg_min = 2
410UserActLabel_v.usage = '<CLs...> <-1|0|1>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400411
412
Mike Frysinger48b5e012020-02-06 17:04:12 -0500413def UserActLabel_cq(opts, *args):
414 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700415 num = args[-1]
Mike Frysinger254f33f2019-12-11 13:54:29 -0500416 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700417 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800418 helper.SetReview(cl, labels={'Commit-Queue': num},
419 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500420 _run_parallel_tasks(task, *args[:-1])
Mike Frysinger48b5e012020-02-06 17:04:12 -0500421UserActLabel_cq.arg_min = 2
422UserActLabel_cq.usage = '<CLs...> <0|1|2>'
Mike Frysinger15b23e42014-12-05 17:00:05 -0500423
424
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700425def UserActSubmit(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800426 """Submit CLs"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500427 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700428 helper, cl = GetGerrit(opts, arg)
429 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500430 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800431UserActSubmit.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400432
433
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700434def UserActAbandon(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800435 """Abandon CLs"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500436 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700437 helper, cl = GetGerrit(opts, arg)
438 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500439 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800440UserActAbandon.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400441
442
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700443def UserActRestore(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800444 """Restore CLs that were abandoned"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500445 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700446 helper, cl = GetGerrit(opts, arg)
447 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500448 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800449UserActRestore.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400450
451
Mike Frysinger88f27292014-06-17 09:40:45 -0700452def UserActReviewers(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800453 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500454 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700455 # Allow for optional leading '~'.
456 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
457 add_list, remove_list, invalid_list = [], [], []
458
459 for x in emails:
460 if not email_validator.match(x):
461 invalid_list.append(x)
462 elif x[0] == '~':
463 remove_list.append(x[1:])
464 else:
465 add_list.append(x)
466
467 if invalid_list:
468 cros_build_lib.Die(
469 'Invalid email address(es): %s' % ', '.join(invalid_list))
470
471 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700472 helper, cl = GetGerrit(opts, cl)
473 helper.SetReviewers(cl, add=add_list, remove=remove_list,
Mike Frysingerc7e28162020-02-25 03:23:04 -0500474 dryrun=opts.dryrun, notify=opts.notify)
Harry Cutts26076b32019-02-26 15:01:29 -0800475UserActReviewers.usage = '<CL> <emails...>'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700476
477
Allen Li38abdaa2017-03-16 13:25:02 -0700478def UserActAssign(opts, cl, assignee):
Harry Cutts26076b32019-02-26 15:01:29 -0800479 """Set the assignee for a CL"""
Allen Li38abdaa2017-03-16 13:25:02 -0700480 helper, cl = GetGerrit(opts, cl)
481 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800482UserActAssign.usage = '<CL> <assignee>'
Allen Li38abdaa2017-03-16 13:25:02 -0700483
484
Mike Frysinger88f27292014-06-17 09:40:45 -0700485def UserActMessage(opts, cl, message):
Harry Cutts26076b32019-02-26 15:01:29 -0800486 """Add a message to a CL"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700487 helper, cl = GetGerrit(opts, cl)
488 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800489UserActMessage.usage = '<CL> <message>'
Doug Anderson8119df02013-07-20 21:00:24 +0530490
491
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800492def UserActTopic(opts, topic, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800493 """Set a topic for one or more CLs"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500494 def task(arg):
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800495 helper, arg = GetGerrit(opts, arg)
496 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500497 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800498UserActTopic.usage = '<topic> <CLs...>'
499
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800500
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700501def UserActPrivate(opts, cl, private_str):
Harry Cutts26076b32019-02-26 15:01:29 -0800502 """Set the private bit on a CL to private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700503 try:
504 private = cros_build_lib.BooleanShellValue(private_str, False)
505 except ValueError:
506 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
507
508 helper, cl = GetGerrit(opts, cl)
Mike Frysingerc1bbdfd2020-02-06 23:54:48 -0500509 helper.SetPrivate(cl, private, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800510UserActPrivate.usage = '<CL> <private str>'
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700511
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800512
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800513def UserActSethashtags(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800514 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800515 hashtags = args
516 add = []
517 remove = []
518 for hashtag in hashtags:
519 if hashtag.startswith('~'):
520 remove.append(hashtag[1:])
521 else:
522 add.append(hashtag)
523 helper, cl = GetGerrit(opts, cl)
524 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800525UserActSethashtags.usage = '<CL> <hashtags...>'
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800526
527
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700528def UserActDeletedraft(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800529 """Delete draft CLs"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500530 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700531 helper, cl = GetGerrit(opts, arg)
532 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500533 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800534UserActDeletedraft.usage = '<CLs...>'
Jon Salza427fb02014-03-07 18:13:17 +0800535
536
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500537def UserActReviewed(opts, *args):
538 """Mark CLs as reviewed"""
539 def task(arg):
540 helper, cl = GetGerrit(opts, arg)
541 helper.ReviewedChange(cl, dryrun=opts.dryrun)
542 _run_parallel_tasks(task, *args)
543UserActReviewed.usage = '<CLs...>'
544
545
546def UserActUnreviewed(opts, *args):
547 """Mark CLs as unreviewed"""
548 def task(arg):
549 helper, cl = GetGerrit(opts, arg)
550 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
551 _run_parallel_tasks(task, *args)
552UserActUnreviewed.usage = '<CLs...>'
553
554
555def UserActIgnore(opts, *args):
556 """Ignore CLs (suppress notifications/dashboard/etc...)"""
557 def task(arg):
558 helper, cl = GetGerrit(opts, arg)
559 helper.IgnoreChange(cl, dryrun=opts.dryrun)
560 _run_parallel_tasks(task, *args)
561UserActIgnore.usage = '<CLs...>'
562
563
564def UserActUnignore(opts, *args):
565 """Unignore CLs (enable notifications/dashboard/etc...)"""
566 def task(arg):
567 helper, cl = GetGerrit(opts, arg)
568 helper.UnignoreChange(cl, dryrun=opts.dryrun)
569 _run_parallel_tasks(task, *args)
570UserActUnignore.usage = '<CLs...>'
571
572
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800573def UserActAccount(opts):
Harry Cutts26076b32019-02-26 15:01:29 -0800574 """Get the current user account information"""
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800575 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400576 acct = helper.GetAccount()
577 if opts.json:
578 json.dump(acct, sys.stdout)
579 else:
580 print('account_id:%i %s <%s>' %
581 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800582
583
Mike Frysinger65fc8632020-02-06 18:11:12 -0500584@memoize.Memoize
585def _GetActions():
586 """Get all the possible actions we support.
587
588 Returns:
589 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
590 function that implements that command (e.g. UserActFoo).
591 """
592 ret = collections.OrderedDict()
593 for funcname in sorted(globals()):
594 if not funcname.startswith(ACTION_PREFIX):
595 continue
596
597 # Turn "UserActFoo" into just "Foo" for further checking below.
598 funcname_chopped = funcname[len(ACTION_PREFIX):]
599
600 # Sanity check names for devs adding new commands. Should be quick.
601 expected_funcname = funcname_chopped.lower().capitalize()
602 if funcname_chopped != expected_funcname:
603 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
604 (funcname_chopped, expected_funcname))
605
606 # Turn "Foo_bar" into "foo-bar".
607 cmdname = funcname_chopped.lower().replace('_', '-')
608 func = globals()[funcname]
609 ret[cmdname] = func
610
611 return ret
612
613
Harry Cutts26076b32019-02-26 15:01:29 -0800614def _GetActionUsages():
615 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -0500616 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -0800617
Mike Frysinger65fc8632020-02-06 18:11:12 -0500618 cmds = list(actions.keys())
619 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -0800620 usages = [getattr(x, 'usage', '') for x in functions]
621 docs = [x.__doc__ for x in functions]
622
Harry Cutts26076b32019-02-26 15:01:29 -0800623 cmd_indent = len(max(cmds, key=len))
624 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500625 return '\n'.join(
626 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
627 for cmd, usage, doc in zip(cmds, usages, docs)
628 )
Harry Cutts26076b32019-02-26 15:01:29 -0800629
630
Mike Frysinger108eda22018-06-06 18:45:12 -0400631def GetParser():
632 """Returns the parser to use for this module."""
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500633 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400634
635There is no support for doing line-by-line code review via the command line.
636This helps you manage various bits and CL status.
637
Mike Frysingera1db2c42014-06-15 00:42:48 -0700638For general Gerrit documentation, see:
639 https://gerrit-review.googlesource.com/Documentation/
640The Searching Changes page covers the search query syntax:
641 https://gerrit-review.googlesource.com/Documentation/user-search.html
642
Mike Frysinger13f23a42013-05-13 17:32:01 -0400643Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500644 $ gerrit todo # List all the CLs that await your review.
645 $ gerrit mine # List all of your open CLs.
646 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
647 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
648 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800649 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
65028123.
651 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
652CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700653Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500654 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
655with Commit-Queue=1.
656 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
657CLs with Commit-Queue=1.
658 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400659
Harry Cutts26076b32019-02-26 15:01:29 -0800660Actions:
661"""
662 usage += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400663
Mike Frysinger65fc8632020-02-06 18:11:12 -0500664 actions = _GetActions()
665
Alex Klein2ab29cc2018-07-19 12:01:00 -0600666 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500667 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500668 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600669 default=site_params.EXTERNAL_GOB_INSTANCE,
670 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500671 help='Query internal Chromium Gerrit instance')
672 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600673 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500674 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600675 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500676 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400677 help='Key to sort on (number, project); use "unsorted" '
678 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700679 parser.add_argument('--raw', default=False, action='store_true',
680 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400681 parser.add_argument('--json', default=False, action='store_true',
682 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700683 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
684 dest='dryrun',
685 help='Show what would be done, but do not make changes')
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800686 parser.add_argument('--ne', '--no-emails', default=True, action='store_false',
687 dest='send_email',
688 help='Do not send email for some operations '
689 '(e.g. ready/review/trybotready/verify)')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500690 parser.add_argument('-v', '--verbose', default=False, action='store_true',
691 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800692 parser.add_argument('-b', '--branch',
693 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700694 parser.add_argument('--draft', default=False, action='store_true',
695 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800696 parser.add_argument('-p', '--project',
697 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800698 parser.add_argument('-t', '--topic',
699 help='Limit output to the specific topic')
Mike Frysinger65fc8632020-02-06 18:11:12 -0500700 parser.add_argument('action', choices=list(actions.keys()),
701 help='The gerrit action to perform')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500702 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400703
704 return parser
705
706
707def main(argv):
708 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500709 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400710
Mike Frysinger88f27292014-06-17 09:40:45 -0700711 # A cache of gerrit helpers we'll load on demand.
712 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800713
714 # Convert user friendly command line option into a gerrit parameter.
715 opts.notify = 'ALL' if opts.send_email else 'NONE'
Mike Frysinger88f27292014-06-17 09:40:45 -0700716 opts.Freeze()
717
Mike Frysinger27e21b72018-07-12 14:20:21 -0400718 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400719 global COLOR
720 COLOR = terminal.Color(enabled=opts.color)
721
Mike Frysinger13f23a42013-05-13 17:32:01 -0400722 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -0500723 actions = _GetActions()
724 functor = actions[opts.action]
725 argspec = inspect.getargspec(functor)
726 if argspec.varargs:
727 arg_min = getattr(functor, 'arg_min', len(argspec.args))
728 if len(opts.args) < arg_min:
729 parser.error('incorrect number of args: %s expects at least %s' %
730 (opts.action, arg_min))
731 elif len(argspec.args) - 1 != len(opts.args):
732 parser.error('incorrect number of args: %s expects %s' %
733 (opts.action, len(argspec.args) - 1))
734 try:
735 functor(opts, *opts.args)
736 except (cros_build_lib.RunCommandError, gerrit.GerritException,
737 gob_util.GOBError) as e:
738 cros_build_lib.Die(e)