blob: 734a84f68800e828305daf261c6e8c515f98eb2a [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 Frysinger108eda22018-06-06 18:45:12 -040034# Locate actions that are exposed to the user. All functions that start
35# with "UserAct" are fair game.
36ACTION_PREFIX = 'UserAct'
37
38
Mike Frysinger254f33f2019-12-11 13:54:29 -050039# How many connections we'll use in parallel. We don't want this to be too high
40# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
41# seems to be good enough for users.
42CONNECTION_LIMIT = 10
43
44
Mike Frysinger031ad0b2013-05-14 18:15:34 -040045COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040046
47# Map the internal names to the ones we normally show on the web ui.
48GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080049 'COMR': ['CQ', 'Commit Queue ',],
50 'CRVW': ['CR', 'Code Review ',],
51 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080052 'VRIF': ['V ', 'Verified ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060053 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040054}
55
56# Order is important -- matches the web ui. This also controls the short
57# entries that we summarize in non-verbose mode.
58GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
59
Mike Frysinger4aea5dc2019-07-17 13:39:56 -040060# Shorter strings for CL status messages.
61GERRIT_SUMMARY_MAP = {
62 'ABANDONED': 'ABD',
63 'MERGED': 'MRG',
64 'NEW': 'NEW',
65 'WIP': 'WIP',
66}
67
Mike Frysinger13f23a42013-05-13 17:32:01 -040068
69def red(s):
70 return COLOR.Color(terminal.Color.RED, s)
71
72
73def green(s):
74 return COLOR.Color(terminal.Color.GREEN, s)
75
76
77def blue(s):
78 return COLOR.Color(terminal.Color.BLUE, s)
79
80
Mike Frysinger254f33f2019-12-11 13:54:29 -050081def _run_parallel_tasks(task, *args):
82 """Small wrapper around BackgroundTaskRunner to enforce job count."""
83 with parallel.BackgroundTaskRunner(task, processes=CONNECTION_LIMIT) as q:
84 for arg in args:
85 q.put([arg])
86
87
Mike Frysinger13f23a42013-05-13 17:32:01 -040088def limits(cls):
89 """Given a dict of fields, calculate the longest string lengths
90
91 This allows you to easily format the output of many results so that the
92 various cols all line up correctly.
93 """
94 lims = {}
95 for cl in cls:
96 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040097 # Use %s rather than str() to avoid codec issues.
98 # We also do this so we can format integers.
99 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400100 return lims
101
102
Mike Frysinger88f27292014-06-17 09:40:45 -0700103# TODO: This func really needs to be merged into the core gerrit logic.
104def GetGerrit(opts, cl=None):
105 """Auto pick the right gerrit instance based on the |cl|
106
107 Args:
108 opts: The general options object.
109 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
110
111 Returns:
112 A tuple of a gerrit object and a sanitized CL #.
113 """
114 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700115 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600116 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600117 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600118 if cl.startswith('*'):
119 cl = cl[1:]
120 else:
121 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700122 elif ':' in cl:
123 gob, cl = cl.split(':', 1)
124
125 if not gob in opts.gerrit:
126 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
127
128 return (opts.gerrit[gob], cl)
129
130
Mike Frysinger13f23a42013-05-13 17:32:01 -0400131def GetApprovalSummary(_opts, cls):
132 """Return a dict of the most important approvals"""
133 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700134 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
135 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
136 if not cats:
137 logging.warning('unknown gerrit approval type: %s', approver['type'])
138 continue
139 cat = cats[0].strip()
140 val = int(approver['value'])
141 if not cat in approvs:
142 # Ignore the extended categories in the summary view.
143 continue
144 elif approvs[cat] == '':
145 approvs[cat] = val
146 elif val < 0:
147 approvs[cat] = min(approvs[cat], val)
148 else:
149 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400150 return approvs
151
152
Mike Frysingera1b4b272017-04-05 16:11:00 -0400153def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400154 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400155 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400156 lims = {'url': 0, 'project': 0}
157
158 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400159
160 if opts.verbose:
161 status += '%s ' % (cl['status'],)
162 else:
163 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
164
Mike Frysinger13f23a42013-05-13 17:32:01 -0400165 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400166 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400167 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400168 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400169 functor = lambda x: x
170 elif approvs[cat] < 0:
171 functor = red
172 else:
173 functor = green
174 status += functor('%s:%2s ' % (cat, approvs[cat]))
175
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400176 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
177 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400178
179 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400180 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400181 functor = red if int(approver['value']) < 0 else green
182 n = functor('%2s' % approver['value'])
183 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
184 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500185 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400186
187
Mike Frysingera1b4b272017-04-05 16:11:00 -0400188def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400189 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400190 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600191 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400192 pfx = ''
193 # Special case internal Chrome GoB as that is what most devs use.
194 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600195 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
196 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400197 for cl in cls:
198 print('%s%s' % (pfx, cl['number']))
199
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400200 elif opts.json:
201 json.dump(cls, sys.stdout)
202
Mike Frysingera1b4b272017-04-05 16:11:00 -0400203 else:
204 if lims is None:
205 lims = limits(cls)
206
207 for cl in cls:
208 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
209
210
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400211def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700212 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800213 if opts.branch is not None:
214 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800215 if opts.project is not None:
216 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800217 if opts.topic is not None:
218 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800219
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400220 if helper is None:
221 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700222 return helper.Query(query, raw=raw, bypass_cache=False)
223
224
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400225def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700226 """Query gerrit and filter/clean up the results"""
227 ret = []
228
Mike Frysinger2cd56022017-01-12 20:56:27 -0500229 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400230 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400231 # Gerrit likes to return a stats record too.
232 if not 'project' in cl:
233 continue
234
235 # Strip off common leading names since the result is still
236 # unique over the whole tree.
237 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400238 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
239 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400240 if cl['project'].startswith('%s/' % pfx):
241 cl['project'] = cl['project'][len(pfx) + 1:]
242
Mike Frysinger479f1192017-09-14 22:36:30 -0400243 cl['url'] = uri_lib.ShortenUri(cl['url'])
244
Mike Frysinger13f23a42013-05-13 17:32:01 -0400245 ret.append(cl)
246
Mike Frysingerb62313a2017-06-30 16:38:58 -0400247 if opts.sort == 'unsorted':
248 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700249 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400250 key = lambda x: int(x[opts.sort])
251 else:
252 key = lambda x: x[opts.sort]
253 return sorted(ret, key=key)
254
255
Mike Frysinger13f23a42013-05-13 17:32:01 -0400256def UserActTodo(opts):
257 """List CLs needing your review"""
Mike Frysinger87690552018-12-30 22:56:06 -0500258 cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
Mike Frysingered3d7ea2017-07-10 13:14:02 -0400259 'label:Code-Review=0,user=self '
260 'NOT label:Verified<0'))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400261 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400262
263
Mike Frysingera1db2c42014-06-15 00:42:48 -0700264def UserActSearch(opts, query):
Harry Cutts26076b32019-02-26 15:01:29 -0800265 """List CLs matching the search query"""
Mike Frysingera1db2c42014-06-15 00:42:48 -0700266 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400267 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800268UserActSearch.usage = '<query>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400269
270
Mike Frysingera1db2c42014-06-15 00:42:48 -0700271def UserActMine(opts):
272 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700273 if opts.draft:
274 rule = 'is:draft'
275 else:
276 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500277 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700278
279
Paul Hobbs89765232015-06-24 14:07:49 -0700280def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
281 """Runs breadth first search starting from the nodes in |to_visit|
282
283 Args:
284 to_visit: the starting nodes
285 children: a function which takes a node and returns the nodes adjacent to it
286 visited_key: a function for deduplicating node visits. Defaults to the
287 identity function (lambda x: x)
288
289 Returns:
290 A list of nodes which are reachable from any node in |to_visit| by calling
291 |children| any number of times.
292 """
293 to_visit = list(to_visit)
Mike Frysinger66ce4132019-07-17 22:52:52 -0400294 seen = set(visited_key(x) for x in to_visit)
Paul Hobbs89765232015-06-24 14:07:49 -0700295 for node in to_visit:
296 for child in children(node):
297 key = visited_key(child)
298 if key not in seen:
299 seen.add(key)
300 to_visit.append(child)
301 return to_visit
302
303
304def UserActDeps(opts, query):
305 """List CLs matching a query, and all transitive dependencies of those CLs"""
306 cls = _Query(opts, query, raw=False)
307
Mike Frysinger10666292018-07-12 01:03:38 -0400308 @memoize.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400309 def _QueryChange(cl, helper=None):
310 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700311
Mike Frysinger5726da92017-09-20 22:14:25 -0400312 def _ProcessDeps(cl, deps, required):
313 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700314 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400315 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400316 if not dep.remote in opts.gerrit:
317 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
318 remote=dep.remote, print_cmd=opts.debug)
319 helper = opts.gerrit[dep.remote]
320
Paul Hobbs89765232015-06-24 14:07:49 -0700321 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400322 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
323
324 # Handle empty results. If we found a commit that was pushed directly
325 # (e.g. a bot commit), then gerrit won't know about it.
326 if not changes:
327 if required:
328 logging.error('CL %s depends on %s which cannot be found',
329 cl, dep.ToGerritQueryText())
330 continue
331
332 # Our query might have matched more than one result. This can come up
333 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
334 # across multiple repos/branches. We blindly check all of them in the
335 # hopes that all open ones are what the user wants, but then again the
336 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
337 if len(changes) > 1:
338 logging.warning('CL %s has an ambiguous CQ dependency %s',
339 cl, dep.ToGerritQueryText())
340 for change in changes:
341 if change.status == 'NEW':
342 yield change
343
344 def _Children(cl):
345 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
346 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
347 yield change
348 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
349 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700350
351 transitives = _BreadthFirstSearch(
352 cls, _Children,
353 visited_key=lambda cl: cl.gerrit_number)
354
355 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400356 PrintCls(opts, transitives_raw)
Harry Cutts26076b32019-02-26 15:01:29 -0800357UserActDeps.usage = '<query>'
Paul Hobbs89765232015-06-24 14:07:49 -0700358
359
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700360def UserActInspect(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800361 """Show the details of one or more CLs"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400362 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700363 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400364 helper, cl = GetGerrit(opts, arg)
365 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
366 if change:
367 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700368 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400369 logging.warning('no results found for CL %s', arg)
370 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800371UserActInspect.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400372
373
Mike Frysinger48b5e012020-02-06 17:04:12 -0500374def UserActLabel_as(opts, *args):
375 """Change the Auto-Submit label"""
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600376 num = args[-1]
Mike Frysinger254f33f2019-12-11 13:54:29 -0500377 def task(arg):
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600378 helper, cl = GetGerrit(opts, arg)
379 helper.SetReview(cl, labels={'Auto-Submit': num},
380 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500381 _run_parallel_tasks(task, *args[:-1])
Mike Frysinger48b5e012020-02-06 17:04:12 -0500382UserActLabel_as.arg_min = 2
383UserActLabel_as.usage = '<CLs...> <0|1>'
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600384
385
Mike Frysinger48b5e012020-02-06 17:04:12 -0500386def UserActLabel_cr(opts, *args):
387 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700388 num = args[-1]
Mike Frysinger254f33f2019-12-11 13:54:29 -0500389 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700390 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800391 helper.SetReview(cl, labels={'Code-Review': num},
392 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500393 _run_parallel_tasks(task, *args[:-1])
Mike Frysinger48b5e012020-02-06 17:04:12 -0500394UserActLabel_cr.arg_min = 2
395UserActLabel_cr.usage = '<CLs...> <-2|-1|0|1|2>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400396
397
Mike Frysinger48b5e012020-02-06 17:04:12 -0500398def UserActLabel_v(opts, *args):
399 """Change the Verified label"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700400 num = args[-1]
Mike Frysinger254f33f2019-12-11 13:54:29 -0500401 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700402 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800403 helper.SetReview(cl, labels={'Verified': num},
404 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500405 _run_parallel_tasks(task, *args[:-1])
Mike Frysinger48b5e012020-02-06 17:04:12 -0500406UserActLabel_v.arg_min = 2
407UserActLabel_v.usage = '<CLs...> <-1|0|1>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400408
409
Mike Frysinger48b5e012020-02-06 17:04:12 -0500410def UserActLabel_cq(opts, *args):
411 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700412 num = args[-1]
Mike Frysinger254f33f2019-12-11 13:54:29 -0500413 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700414 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800415 helper.SetReview(cl, labels={'Commit-Queue': num},
416 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500417 _run_parallel_tasks(task, *args[:-1])
Mike Frysinger48b5e012020-02-06 17:04:12 -0500418UserActLabel_cq.arg_min = 2
419UserActLabel_cq.usage = '<CLs...> <0|1|2>'
Mike Frysinger15b23e42014-12-05 17:00:05 -0500420
421
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700422def UserActSubmit(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800423 """Submit CLs"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500424 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700425 helper, cl = GetGerrit(opts, arg)
426 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500427 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800428UserActSubmit.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400429
430
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700431def UserActAbandon(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800432 """Abandon CLs"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500433 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700434 helper, cl = GetGerrit(opts, arg)
435 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500436 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800437UserActAbandon.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400438
439
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700440def UserActRestore(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800441 """Restore CLs that were abandoned"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500442 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700443 helper, cl = GetGerrit(opts, arg)
444 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500445 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800446UserActRestore.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400447
448
Mike Frysinger88f27292014-06-17 09:40:45 -0700449def UserActReviewers(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800450 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500451 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700452 # Allow for optional leading '~'.
453 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
454 add_list, remove_list, invalid_list = [], [], []
455
456 for x in emails:
457 if not email_validator.match(x):
458 invalid_list.append(x)
459 elif x[0] == '~':
460 remove_list.append(x[1:])
461 else:
462 add_list.append(x)
463
464 if invalid_list:
465 cros_build_lib.Die(
466 'Invalid email address(es): %s' % ', '.join(invalid_list))
467
468 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700469 helper, cl = GetGerrit(opts, cl)
470 helper.SetReviewers(cl, add=add_list, remove=remove_list,
471 dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800472UserActReviewers.usage = '<CL> <emails...>'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700473
474
Allen Li38abdaa2017-03-16 13:25:02 -0700475def UserActAssign(opts, cl, assignee):
Harry Cutts26076b32019-02-26 15:01:29 -0800476 """Set the assignee for a CL"""
Allen Li38abdaa2017-03-16 13:25:02 -0700477 helper, cl = GetGerrit(opts, cl)
478 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800479UserActAssign.usage = '<CL> <assignee>'
Allen Li38abdaa2017-03-16 13:25:02 -0700480
481
Mike Frysinger88f27292014-06-17 09:40:45 -0700482def UserActMessage(opts, cl, message):
Harry Cutts26076b32019-02-26 15:01:29 -0800483 """Add a message to a CL"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700484 helper, cl = GetGerrit(opts, cl)
485 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800486UserActMessage.usage = '<CL> <message>'
Doug Anderson8119df02013-07-20 21:00:24 +0530487
488
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800489def UserActTopic(opts, topic, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800490 """Set a topic for one or more CLs"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500491 def task(arg):
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800492 helper, arg = GetGerrit(opts, arg)
493 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500494 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800495UserActTopic.usage = '<topic> <CLs...>'
496
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800497
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700498def UserActPrivate(opts, cl, private_str):
Harry Cutts26076b32019-02-26 15:01:29 -0800499 """Set the private bit on a CL to private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700500 try:
501 private = cros_build_lib.BooleanShellValue(private_str, False)
502 except ValueError:
503 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
504
505 helper, cl = GetGerrit(opts, cl)
Mike Frysingerc1bbdfd2020-02-06 23:54:48 -0500506 helper.SetPrivate(cl, private, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800507UserActPrivate.usage = '<CL> <private str>'
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700508
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800509
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800510def UserActSethashtags(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800511 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800512 hashtags = args
513 add = []
514 remove = []
515 for hashtag in hashtags:
516 if hashtag.startswith('~'):
517 remove.append(hashtag[1:])
518 else:
519 add.append(hashtag)
520 helper, cl = GetGerrit(opts, cl)
521 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800522UserActSethashtags.usage = '<CL> <hashtags...>'
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800523
524
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700525def UserActDeletedraft(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800526 """Delete draft CLs"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500527 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700528 helper, cl = GetGerrit(opts, arg)
529 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500530 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800531UserActDeletedraft.usage = '<CLs...>'
Jon Salza427fb02014-03-07 18:13:17 +0800532
533
Mike Frysinger6f04dd42020-02-06 16:48:18 -0500534def UserActReviewed(opts, *args):
535 """Mark CLs as reviewed"""
536 def task(arg):
537 helper, cl = GetGerrit(opts, arg)
538 helper.ReviewedChange(cl, dryrun=opts.dryrun)
539 _run_parallel_tasks(task, *args)
540UserActReviewed.usage = '<CLs...>'
541
542
543def UserActUnreviewed(opts, *args):
544 """Mark CLs as unreviewed"""
545 def task(arg):
546 helper, cl = GetGerrit(opts, arg)
547 helper.UnreviewedChange(cl, dryrun=opts.dryrun)
548 _run_parallel_tasks(task, *args)
549UserActUnreviewed.usage = '<CLs...>'
550
551
552def UserActIgnore(opts, *args):
553 """Ignore CLs (suppress notifications/dashboard/etc...)"""
554 def task(arg):
555 helper, cl = GetGerrit(opts, arg)
556 helper.IgnoreChange(cl, dryrun=opts.dryrun)
557 _run_parallel_tasks(task, *args)
558UserActIgnore.usage = '<CLs...>'
559
560
561def UserActUnignore(opts, *args):
562 """Unignore CLs (enable notifications/dashboard/etc...)"""
563 def task(arg):
564 helper, cl = GetGerrit(opts, arg)
565 helper.UnignoreChange(cl, dryrun=opts.dryrun)
566 _run_parallel_tasks(task, *args)
567UserActUnignore.usage = '<CLs...>'
568
569
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800570def UserActAccount(opts):
Harry Cutts26076b32019-02-26 15:01:29 -0800571 """Get the current user account information"""
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800572 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400573 acct = helper.GetAccount()
574 if opts.json:
575 json.dump(acct, sys.stdout)
576 else:
577 print('account_id:%i %s <%s>' %
578 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800579
580
Mike Frysinger65fc8632020-02-06 18:11:12 -0500581@memoize.Memoize
582def _GetActions():
583 """Get all the possible actions we support.
584
585 Returns:
586 An ordered dictionary mapping the user subcommand (e.g. "foo") to the
587 function that implements that command (e.g. UserActFoo).
588 """
589 ret = collections.OrderedDict()
590 for funcname in sorted(globals()):
591 if not funcname.startswith(ACTION_PREFIX):
592 continue
593
594 # Turn "UserActFoo" into just "Foo" for further checking below.
595 funcname_chopped = funcname[len(ACTION_PREFIX):]
596
597 # Sanity check names for devs adding new commands. Should be quick.
598 expected_funcname = funcname_chopped.lower().capitalize()
599 if funcname_chopped != expected_funcname:
600 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
601 (funcname_chopped, expected_funcname))
602
603 # Turn "Foo_bar" into "foo-bar".
604 cmdname = funcname_chopped.lower().replace('_', '-')
605 func = globals()[funcname]
606 ret[cmdname] = func
607
608 return ret
609
610
Harry Cutts26076b32019-02-26 15:01:29 -0800611def _GetActionUsages():
612 """Formats a one-line usage and doc message for each action."""
Mike Frysinger65fc8632020-02-06 18:11:12 -0500613 actions = _GetActions()
Harry Cutts26076b32019-02-26 15:01:29 -0800614
Mike Frysinger65fc8632020-02-06 18:11:12 -0500615 cmds = list(actions.keys())
616 functions = list(actions.values())
Harry Cutts26076b32019-02-26 15:01:29 -0800617 usages = [getattr(x, 'usage', '') for x in functions]
618 docs = [x.__doc__ for x in functions]
619
Harry Cutts26076b32019-02-26 15:01:29 -0800620 cmd_indent = len(max(cmds, key=len))
621 usage_indent = len(max(usages, key=len))
Mike Frysinger65fc8632020-02-06 18:11:12 -0500622 return '\n'.join(
623 ' %-*s %-*s : %s' % (cmd_indent, cmd, usage_indent, usage, doc)
624 for cmd, usage, doc in zip(cmds, usages, docs)
625 )
Harry Cutts26076b32019-02-26 15:01:29 -0800626
627
Mike Frysinger108eda22018-06-06 18:45:12 -0400628def GetParser():
629 """Returns the parser to use for this module."""
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500630 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400631
632There is no support for doing line-by-line code review via the command line.
633This helps you manage various bits and CL status.
634
Mike Frysingera1db2c42014-06-15 00:42:48 -0700635For general Gerrit documentation, see:
636 https://gerrit-review.googlesource.com/Documentation/
637The Searching Changes page covers the search query syntax:
638 https://gerrit-review.googlesource.com/Documentation/user-search.html
639
Mike Frysinger13f23a42013-05-13 17:32:01 -0400640Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500641 $ gerrit todo # List all the CLs that await your review.
642 $ gerrit mine # List all of your open CLs.
643 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
644 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
645 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800646 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
64728123.
648 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
649CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700650Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500651 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
652with Commit-Queue=1.
653 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
654CLs with Commit-Queue=1.
655 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400656
Harry Cutts26076b32019-02-26 15:01:29 -0800657Actions:
658"""
659 usage += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400660
Mike Frysinger65fc8632020-02-06 18:11:12 -0500661 actions = _GetActions()
662
Alex Klein2ab29cc2018-07-19 12:01:00 -0600663 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500664 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500665 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600666 default=site_params.EXTERNAL_GOB_INSTANCE,
667 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500668 help='Query internal Chromium Gerrit instance')
669 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600670 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500671 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600672 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500673 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400674 help='Key to sort on (number, project); use "unsorted" '
675 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700676 parser.add_argument('--raw', default=False, action='store_true',
677 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400678 parser.add_argument('--json', default=False, action='store_true',
679 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700680 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
681 dest='dryrun',
682 help='Show what would be done, but do not make changes')
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800683 parser.add_argument('--ne', '--no-emails', default=True, action='store_false',
684 dest='send_email',
685 help='Do not send email for some operations '
686 '(e.g. ready/review/trybotready/verify)')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500687 parser.add_argument('-v', '--verbose', default=False, action='store_true',
688 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800689 parser.add_argument('-b', '--branch',
690 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700691 parser.add_argument('--draft', default=False, action='store_true',
692 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800693 parser.add_argument('-p', '--project',
694 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800695 parser.add_argument('-t', '--topic',
696 help='Limit output to the specific topic')
Mike Frysinger65fc8632020-02-06 18:11:12 -0500697 parser.add_argument('action', choices=list(actions.keys()),
698 help='The gerrit action to perform')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500699 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400700
701 return parser
702
703
704def main(argv):
705 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500706 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400707
Mike Frysinger88f27292014-06-17 09:40:45 -0700708 # A cache of gerrit helpers we'll load on demand.
709 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800710
711 # Convert user friendly command line option into a gerrit parameter.
712 opts.notify = 'ALL' if opts.send_email else 'NONE'
Mike Frysinger88f27292014-06-17 09:40:45 -0700713 opts.Freeze()
714
Mike Frysinger27e21b72018-07-12 14:20:21 -0400715 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400716 global COLOR
717 COLOR = terminal.Color(enabled=opts.color)
718
Mike Frysinger13f23a42013-05-13 17:32:01 -0400719 # Now look up the requested user action and run it.
Mike Frysinger65fc8632020-02-06 18:11:12 -0500720 actions = _GetActions()
721 functor = actions[opts.action]
722 argspec = inspect.getargspec(functor)
723 if argspec.varargs:
724 arg_min = getattr(functor, 'arg_min', len(argspec.args))
725 if len(opts.args) < arg_min:
726 parser.error('incorrect number of args: %s expects at least %s' %
727 (opts.action, arg_min))
728 elif len(argspec.args) - 1 != len(opts.args):
729 parser.error('incorrect number of args: %s expects %s' %
730 (opts.action, len(argspec.args) - 1))
731 try:
732 functor(opts, *opts.args)
733 except (cros_build_lib.RunCommandError, gerrit.GerritException,
734 gob_util.GOBError) as e:
735 cros_build_lib.Die(e)