blob: 9ca4788a92b1c6bbad8f2102b598bd1c288906f0 [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 Frysinger13f23a42013-05-13 17:32:01 -040015import inspect
Mike Frysinger87c74ce2017-04-04 16:12:31 -040016import json
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070017import re
Mike Frysinger87c74ce2017-04-04 16:12:31 -040018import sys
Mike Frysinger13f23a42013-05-13 17:32:01 -040019
Aviv Keshetb7519e12016-10-04 00:50:00 -070020from chromite.lib import config_lib
21from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040022from chromite.lib import commandline
23from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070024from chromite.lib import cros_logging as logging
Mike Frysinger13f23a42013-05-13 17:32:01 -040025from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050026from chromite.lib import gob_util
Mike Frysinger254f33f2019-12-11 13:54:29 -050027from chromite.lib import parallel
Mike Frysinger13f23a42013-05-13 17:32:01 -040028from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040029from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060030from chromite.utils import memoize
Mike Frysinger13f23a42013-05-13 17:32:01 -040031
32
Mike Frysinger108eda22018-06-06 18:45:12 -040033# Locate actions that are exposed to the user. All functions that start
34# with "UserAct" are fair game.
35ACTION_PREFIX = 'UserAct'
36
37
Mike Frysinger254f33f2019-12-11 13:54:29 -050038# How many connections we'll use in parallel. We don't want this to be too high
39# so we don't go over our per-user quota. Pick 10 somewhat arbitrarily as that
40# seems to be good enough for users.
41CONNECTION_LIMIT = 10
42
43
Mike Frysinger031ad0b2013-05-14 18:15:34 -040044COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040045
46# Map the internal names to the ones we normally show on the web ui.
47GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080048 'COMR': ['CQ', 'Commit Queue ',],
49 'CRVW': ['CR', 'Code Review ',],
50 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080051 'VRIF': ['V ', 'Verified ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060052 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040053}
54
55# Order is important -- matches the web ui. This also controls the short
56# entries that we summarize in non-verbose mode.
57GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
58
Mike Frysinger4aea5dc2019-07-17 13:39:56 -040059# Shorter strings for CL status messages.
60GERRIT_SUMMARY_MAP = {
61 'ABANDONED': 'ABD',
62 'MERGED': 'MRG',
63 'NEW': 'NEW',
64 'WIP': 'WIP',
65}
66
Mike Frysinger13f23a42013-05-13 17:32:01 -040067
68def red(s):
69 return COLOR.Color(terminal.Color.RED, s)
70
71
72def green(s):
73 return COLOR.Color(terminal.Color.GREEN, s)
74
75
76def blue(s):
77 return COLOR.Color(terminal.Color.BLUE, s)
78
79
Mike Frysinger254f33f2019-12-11 13:54:29 -050080def _run_parallel_tasks(task, *args):
81 """Small wrapper around BackgroundTaskRunner to enforce job count."""
82 with parallel.BackgroundTaskRunner(task, processes=CONNECTION_LIMIT) as q:
83 for arg in args:
84 q.put([arg])
85
86
Mike Frysinger13f23a42013-05-13 17:32:01 -040087def limits(cls):
88 """Given a dict of fields, calculate the longest string lengths
89
90 This allows you to easily format the output of many results so that the
91 various cols all line up correctly.
92 """
93 lims = {}
94 for cl in cls:
95 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040096 # Use %s rather than str() to avoid codec issues.
97 # We also do this so we can format integers.
98 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040099 return lims
100
101
Mike Frysinger88f27292014-06-17 09:40:45 -0700102# TODO: This func really needs to be merged into the core gerrit logic.
103def GetGerrit(opts, cl=None):
104 """Auto pick the right gerrit instance based on the |cl|
105
106 Args:
107 opts: The general options object.
108 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
109
110 Returns:
111 A tuple of a gerrit object and a sanitized CL #.
112 """
113 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700114 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600115 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600116 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600117 if cl.startswith('*'):
118 cl = cl[1:]
119 else:
120 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700121 elif ':' in cl:
122 gob, cl = cl.split(':', 1)
123
124 if not gob in opts.gerrit:
125 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
126
127 return (opts.gerrit[gob], cl)
128
129
Mike Frysinger13f23a42013-05-13 17:32:01 -0400130def GetApprovalSummary(_opts, cls):
131 """Return a dict of the most important approvals"""
132 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700133 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
134 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
135 if not cats:
136 logging.warning('unknown gerrit approval type: %s', approver['type'])
137 continue
138 cat = cats[0].strip()
139 val = int(approver['value'])
140 if not cat in approvs:
141 # Ignore the extended categories in the summary view.
142 continue
143 elif approvs[cat] == '':
144 approvs[cat] = val
145 elif val < 0:
146 approvs[cat] = min(approvs[cat], val)
147 else:
148 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400149 return approvs
150
151
Mike Frysingera1b4b272017-04-05 16:11:00 -0400152def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400153 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400154 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400155 lims = {'url': 0, 'project': 0}
156
157 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400158
159 if opts.verbose:
160 status += '%s ' % (cl['status'],)
161 else:
162 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
163
Mike Frysinger13f23a42013-05-13 17:32:01 -0400164 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400165 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400166 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400167 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400168 functor = lambda x: x
169 elif approvs[cat] < 0:
170 functor = red
171 else:
172 functor = green
173 status += functor('%s:%2s ' % (cat, approvs[cat]))
174
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400175 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
176 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400177
178 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400179 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400180 functor = red if int(approver['value']) < 0 else green
181 n = functor('%2s' % approver['value'])
182 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
183 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500184 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400185
186
Mike Frysingera1b4b272017-04-05 16:11:00 -0400187def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400188 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400189 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600190 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400191 pfx = ''
192 # Special case internal Chrome GoB as that is what most devs use.
193 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600194 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
195 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400196 for cl in cls:
197 print('%s%s' % (pfx, cl['number']))
198
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400199 elif opts.json:
200 json.dump(cls, sys.stdout)
201
Mike Frysingera1b4b272017-04-05 16:11:00 -0400202 else:
203 if lims is None:
204 lims = limits(cls)
205
206 for cl in cls:
207 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
208
209
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400210def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700211 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800212 if opts.branch is not None:
213 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800214 if opts.project is not None:
215 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800216 if opts.topic is not None:
217 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800218
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400219 if helper is None:
220 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700221 return helper.Query(query, raw=raw, bypass_cache=False)
222
223
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400224def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700225 """Query gerrit and filter/clean up the results"""
226 ret = []
227
Mike Frysinger2cd56022017-01-12 20:56:27 -0500228 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400229 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400230 # Gerrit likes to return a stats record too.
231 if not 'project' in cl:
232 continue
233
234 # Strip off common leading names since the result is still
235 # unique over the whole tree.
236 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400237 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
238 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400239 if cl['project'].startswith('%s/' % pfx):
240 cl['project'] = cl['project'][len(pfx) + 1:]
241
Mike Frysinger479f1192017-09-14 22:36:30 -0400242 cl['url'] = uri_lib.ShortenUri(cl['url'])
243
Mike Frysinger13f23a42013-05-13 17:32:01 -0400244 ret.append(cl)
245
Mike Frysingerb62313a2017-06-30 16:38:58 -0400246 if opts.sort == 'unsorted':
247 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700248 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400249 key = lambda x: int(x[opts.sort])
250 else:
251 key = lambda x: x[opts.sort]
252 return sorted(ret, key=key)
253
254
Mike Frysinger13f23a42013-05-13 17:32:01 -0400255def UserActTodo(opts):
256 """List CLs needing your review"""
Mike Frysinger87690552018-12-30 22:56:06 -0500257 cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
Mike Frysingered3d7ea2017-07-10 13:14:02 -0400258 'label:Code-Review=0,user=self '
259 'NOT label:Verified<0'))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400260 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400261
262
Mike Frysingera1db2c42014-06-15 00:42:48 -0700263def UserActSearch(opts, query):
Harry Cutts26076b32019-02-26 15:01:29 -0800264 """List CLs matching the search query"""
Mike Frysingera1db2c42014-06-15 00:42:48 -0700265 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400266 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800267UserActSearch.usage = '<query>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400268
269
Mike Frysingera1db2c42014-06-15 00:42:48 -0700270def UserActMine(opts):
271 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700272 if opts.draft:
273 rule = 'is:draft'
274 else:
275 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500276 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700277
278
Paul Hobbs89765232015-06-24 14:07:49 -0700279def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
280 """Runs breadth first search starting from the nodes in |to_visit|
281
282 Args:
283 to_visit: the starting nodes
284 children: a function which takes a node and returns the nodes adjacent to it
285 visited_key: a function for deduplicating node visits. Defaults to the
286 identity function (lambda x: x)
287
288 Returns:
289 A list of nodes which are reachable from any node in |to_visit| by calling
290 |children| any number of times.
291 """
292 to_visit = list(to_visit)
Mike Frysinger66ce4132019-07-17 22:52:52 -0400293 seen = set(visited_key(x) for x in to_visit)
Paul Hobbs89765232015-06-24 14:07:49 -0700294 for node in to_visit:
295 for child in children(node):
296 key = visited_key(child)
297 if key not in seen:
298 seen.add(key)
299 to_visit.append(child)
300 return to_visit
301
302
303def UserActDeps(opts, query):
304 """List CLs matching a query, and all transitive dependencies of those CLs"""
305 cls = _Query(opts, query, raw=False)
306
Mike Frysinger10666292018-07-12 01:03:38 -0400307 @memoize.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400308 def _QueryChange(cl, helper=None):
309 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700310
Mike Frysinger5726da92017-09-20 22:14:25 -0400311 def _ProcessDeps(cl, deps, required):
312 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700313 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400314 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400315 if not dep.remote in opts.gerrit:
316 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
317 remote=dep.remote, print_cmd=opts.debug)
318 helper = opts.gerrit[dep.remote]
319
Paul Hobbs89765232015-06-24 14:07:49 -0700320 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400321 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
322
323 # Handle empty results. If we found a commit that was pushed directly
324 # (e.g. a bot commit), then gerrit won't know about it.
325 if not changes:
326 if required:
327 logging.error('CL %s depends on %s which cannot be found',
328 cl, dep.ToGerritQueryText())
329 continue
330
331 # Our query might have matched more than one result. This can come up
332 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
333 # across multiple repos/branches. We blindly check all of them in the
334 # hopes that all open ones are what the user wants, but then again the
335 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
336 if len(changes) > 1:
337 logging.warning('CL %s has an ambiguous CQ dependency %s',
338 cl, dep.ToGerritQueryText())
339 for change in changes:
340 if change.status == 'NEW':
341 yield change
342
343 def _Children(cl):
344 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
345 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
346 yield change
347 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
348 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700349
350 transitives = _BreadthFirstSearch(
351 cls, _Children,
352 visited_key=lambda cl: cl.gerrit_number)
353
354 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400355 PrintCls(opts, transitives_raw)
Harry Cutts26076b32019-02-26 15:01:29 -0800356UserActDeps.usage = '<query>'
Paul Hobbs89765232015-06-24 14:07:49 -0700357
358
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700359def UserActInspect(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800360 """Show the details of one or more CLs"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400361 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700362 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400363 helper, cl = GetGerrit(opts, arg)
364 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
365 if change:
366 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700367 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400368 logging.warning('no results found for CL %s', arg)
369 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800370UserActInspect.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400371
372
Mike Frysinger48b5e012020-02-06 17:04:12 -0500373def UserActLabel_as(opts, *args):
374 """Change the Auto-Submit label"""
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600375 num = args[-1]
Mike Frysinger254f33f2019-12-11 13:54:29 -0500376 def task(arg):
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600377 helper, cl = GetGerrit(opts, arg)
378 helper.SetReview(cl, labels={'Auto-Submit': num},
379 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500380 _run_parallel_tasks(task, *args[:-1])
Mike Frysinger48b5e012020-02-06 17:04:12 -0500381UserActLabel_as.arg_min = 2
382UserActLabel_as.usage = '<CLs...> <0|1>'
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600383
384
Mike Frysinger48b5e012020-02-06 17:04:12 -0500385def UserActLabel_cr(opts, *args):
386 """Change the Code-Review label (1=LGTM 2=LGTM+Approved)"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700387 num = args[-1]
Mike Frysinger254f33f2019-12-11 13:54:29 -0500388 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700389 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800390 helper.SetReview(cl, labels={'Code-Review': num},
391 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500392 _run_parallel_tasks(task, *args[:-1])
Mike Frysinger48b5e012020-02-06 17:04:12 -0500393UserActLabel_cr.arg_min = 2
394UserActLabel_cr.usage = '<CLs...> <-2|-1|0|1|2>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400395
396
Mike Frysinger48b5e012020-02-06 17:04:12 -0500397def UserActLabel_v(opts, *args):
398 """Change the Verified label"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700399 num = args[-1]
Mike Frysinger254f33f2019-12-11 13:54:29 -0500400 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700401 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800402 helper.SetReview(cl, labels={'Verified': num},
403 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500404 _run_parallel_tasks(task, *args[:-1])
Mike Frysinger48b5e012020-02-06 17:04:12 -0500405UserActLabel_v.arg_min = 2
406UserActLabel_v.usage = '<CLs...> <-1|0|1>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400407
408
Mike Frysinger48b5e012020-02-06 17:04:12 -0500409def UserActLabel_cq(opts, *args):
410 """Change the Commit-Queue label (1=dry-run 2=commit)"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700411 num = args[-1]
Mike Frysinger254f33f2019-12-11 13:54:29 -0500412 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700413 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800414 helper.SetReview(cl, labels={'Commit-Queue': num},
415 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500416 _run_parallel_tasks(task, *args[:-1])
Mike Frysinger48b5e012020-02-06 17:04:12 -0500417UserActLabel_cq.arg_min = 2
418UserActLabel_cq.usage = '<CLs...> <0|1|2>'
Mike Frysinger15b23e42014-12-05 17:00:05 -0500419
420
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700421def UserActSubmit(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800422 """Submit CLs"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500423 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700424 helper, cl = GetGerrit(opts, arg)
425 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500426 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800427UserActSubmit.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400428
429
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700430def UserActAbandon(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800431 """Abandon CLs"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500432 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700433 helper, cl = GetGerrit(opts, arg)
434 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500435 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800436UserActAbandon.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400437
438
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700439def UserActRestore(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800440 """Restore CLs that were abandoned"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500441 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700442 helper, cl = GetGerrit(opts, arg)
443 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500444 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800445UserActRestore.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400446
447
Mike Frysinger88f27292014-06-17 09:40:45 -0700448def UserActReviewers(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800449 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500450 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700451 # Allow for optional leading '~'.
452 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
453 add_list, remove_list, invalid_list = [], [], []
454
455 for x in emails:
456 if not email_validator.match(x):
457 invalid_list.append(x)
458 elif x[0] == '~':
459 remove_list.append(x[1:])
460 else:
461 add_list.append(x)
462
463 if invalid_list:
464 cros_build_lib.Die(
465 'Invalid email address(es): %s' % ', '.join(invalid_list))
466
467 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700468 helper, cl = GetGerrit(opts, cl)
469 helper.SetReviewers(cl, add=add_list, remove=remove_list,
470 dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800471UserActReviewers.usage = '<CL> <emails...>'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700472
473
Allen Li38abdaa2017-03-16 13:25:02 -0700474def UserActAssign(opts, cl, assignee):
Harry Cutts26076b32019-02-26 15:01:29 -0800475 """Set the assignee for a CL"""
Allen Li38abdaa2017-03-16 13:25:02 -0700476 helper, cl = GetGerrit(opts, cl)
477 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800478UserActAssign.usage = '<CL> <assignee>'
Allen Li38abdaa2017-03-16 13:25:02 -0700479
480
Mike Frysinger88f27292014-06-17 09:40:45 -0700481def UserActMessage(opts, cl, message):
Harry Cutts26076b32019-02-26 15:01:29 -0800482 """Add a message to a CL"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700483 helper, cl = GetGerrit(opts, cl)
484 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800485UserActMessage.usage = '<CL> <message>'
Doug Anderson8119df02013-07-20 21:00:24 +0530486
487
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800488def UserActTopic(opts, topic, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800489 """Set a topic for one or more CLs"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500490 def task(arg):
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800491 helper, arg = GetGerrit(opts, arg)
492 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500493 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800494UserActTopic.usage = '<topic> <CLs...>'
495
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800496
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700497def UserActPrivate(opts, cl, private_str):
Harry Cutts26076b32019-02-26 15:01:29 -0800498 """Set the private bit on a CL to private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700499 try:
500 private = cros_build_lib.BooleanShellValue(private_str, False)
501 except ValueError:
502 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
503
504 helper, cl = GetGerrit(opts, cl)
505 helper.SetPrivate(cl, private)
Harry Cutts26076b32019-02-26 15:01:29 -0800506UserActPrivate.usage = '<CL> <private str>'
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700507
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800508
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800509def UserActSethashtags(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800510 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800511 hashtags = args
512 add = []
513 remove = []
514 for hashtag in hashtags:
515 if hashtag.startswith('~'):
516 remove.append(hashtag[1:])
517 else:
518 add.append(hashtag)
519 helper, cl = GetGerrit(opts, cl)
520 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800521UserActSethashtags.usage = '<CL> <hashtags...>'
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800522
523
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700524def UserActDeletedraft(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800525 """Delete draft CLs"""
Mike Frysinger254f33f2019-12-11 13:54:29 -0500526 def task(arg):
Mike Frysinger88f27292014-06-17 09:40:45 -0700527 helper, cl = GetGerrit(opts, arg)
528 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Mike Frysinger254f33f2019-12-11 13:54:29 -0500529 _run_parallel_tasks(task, *args)
Harry Cutts26076b32019-02-26 15:01:29 -0800530UserActDeletedraft.usage = '<CLs...>'
Jon Salza427fb02014-03-07 18:13:17 +0800531
532
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800533def UserActAccount(opts):
Harry Cutts26076b32019-02-26 15:01:29 -0800534 """Get the current user account information"""
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800535 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400536 acct = helper.GetAccount()
537 if opts.json:
538 json.dump(acct, sys.stdout)
539 else:
540 print('account_id:%i %s <%s>' %
541 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800542
543
Harry Cutts26076b32019-02-26 15:01:29 -0800544def _GetActionUsages():
545 """Formats a one-line usage and doc message for each action."""
546 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
547 actions.sort()
548
549 cmds = [x[len(ACTION_PREFIX):] for x in actions]
550
551 # Sanity check names for devs adding new commands. Should be quick.
552 for cmd in cmds:
553 expected_name = cmd.lower().capitalize()
554 if cmd != expected_name:
555 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
556 (cmd, expected_name))
557
558 functions = [globals()[x] for x in actions]
559 usages = [getattr(x, 'usage', '') for x in functions]
560 docs = [x.__doc__ for x in functions]
561
562 action_usages = []
563 cmd_indent = len(max(cmds, key=len))
564 usage_indent = len(max(usages, key=len))
565 for cmd, usage, doc in zip(cmds, usages, docs):
Mike Frysinger48b5e012020-02-06 17:04:12 -0500566 action_usages.append(
567 ' %-*s %-*s : %s' %
568 (cmd_indent, cmd.lower().replace('_', '-'), usage_indent, usage, doc))
Harry Cutts26076b32019-02-26 15:01:29 -0800569
570 return '\n'.join(action_usages)
571
572
Mike Frysinger108eda22018-06-06 18:45:12 -0400573def GetParser():
574 """Returns the parser to use for this module."""
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500575 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400576
577There is no support for doing line-by-line code review via the command line.
578This helps you manage various bits and CL status.
579
Mike Frysingera1db2c42014-06-15 00:42:48 -0700580For general Gerrit documentation, see:
581 https://gerrit-review.googlesource.com/Documentation/
582The Searching Changes page covers the search query syntax:
583 https://gerrit-review.googlesource.com/Documentation/user-search.html
584
Mike Frysinger13f23a42013-05-13 17:32:01 -0400585Example:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500586 $ gerrit todo # List all the CLs that await your review.
587 $ gerrit mine # List all of your open CLs.
588 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
589 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
590 $ gerrit label-v 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800591 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
59228123.
593 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
594CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700595Scripting:
Mike Frysinger48b5e012020-02-06 17:04:12 -0500596 $ gerrit label-cq `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
597with Commit-Queue=1.
598 $ gerrit label-cq `gerrit --raw -i mine` 1 # Mark *ALL* of your internal \
599CLs with Commit-Queue=1.
600 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400601
Harry Cutts26076b32019-02-26 15:01:29 -0800602Actions:
603"""
604 usage += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400605
Alex Klein2ab29cc2018-07-19 12:01:00 -0600606 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500607 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500608 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600609 default=site_params.EXTERNAL_GOB_INSTANCE,
610 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500611 help='Query internal Chromium Gerrit instance')
612 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600613 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500614 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600615 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500616 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400617 help='Key to sort on (number, project); use "unsorted" '
618 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700619 parser.add_argument('--raw', default=False, action='store_true',
620 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400621 parser.add_argument('--json', default=False, action='store_true',
622 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700623 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
624 dest='dryrun',
625 help='Show what would be done, but do not make changes')
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800626 parser.add_argument('--ne', '--no-emails', default=True, action='store_false',
627 dest='send_email',
628 help='Do not send email for some operations '
629 '(e.g. ready/review/trybotready/verify)')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500630 parser.add_argument('-v', '--verbose', default=False, action='store_true',
631 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800632 parser.add_argument('-b', '--branch',
633 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700634 parser.add_argument('--draft', default=False, action='store_true',
635 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800636 parser.add_argument('-p', '--project',
637 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800638 parser.add_argument('-t', '--topic',
639 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500640 parser.add_argument('action', help='The gerrit action to perform')
641 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400642
643 return parser
644
645
646def main(argv):
647 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500648 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400649
Mike Frysinger88f27292014-06-17 09:40:45 -0700650 # A cache of gerrit helpers we'll load on demand.
651 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800652
653 # Convert user friendly command line option into a gerrit parameter.
654 opts.notify = 'ALL' if opts.send_email else 'NONE'
Mike Frysinger88f27292014-06-17 09:40:45 -0700655 opts.Freeze()
656
Mike Frysinger27e21b72018-07-12 14:20:21 -0400657 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400658 global COLOR
659 COLOR = terminal.Color(enabled=opts.color)
660
Mike Frysinger13f23a42013-05-13 17:32:01 -0400661 # Now look up the requested user action and run it.
Mike Frysinger48b5e012020-02-06 17:04:12 -0500662 funcname = ACTION_PREFIX + opts.action.capitalize().replace('-', '_')
663 functor = globals().get(funcname)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400664 if functor:
665 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700666 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700667 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500668 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700669 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500670 (opts.action, arg_min))
671 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400672 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500673 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700674 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500675 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500676 except (cros_build_lib.RunCommandError, gerrit.GerritException,
677 gob_util.GOBError) as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400678 cros_build_lib.Die(e)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400679 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500680 parser.error('unknown action: %s' % (opts.action,))