blob: 5d9812e424dee4d332c57d505f92d582e0c6e65c [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
Mathieu Olivari04b4d522014-12-18 17:26:34 -080026from chromite.lib import git
Mike Frysingerc85d8162014-02-08 00:45:21 -050027from chromite.lib import gob_util
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
Mike Frysinger13f23a42013-05-13 17:32:01 -040030
31
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -070032site_config = config_lib.GetConfig()
33
34
Mike Frysinger108eda22018-06-06 18:45:12 -040035# Locate actions that are exposed to the user. All functions that start
36# with "UserAct" are fair game.
37ACTION_PREFIX = 'UserAct'
38
39
Mike Frysinger031ad0b2013-05-14 18:15:34 -040040COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040041
42# Map the internal names to the ones we normally show on the web ui.
43GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080044 'COMR': ['CQ', 'Commit Queue ',],
45 'CRVW': ['CR', 'Code Review ',],
46 'SUBM': ['S ', 'Submitted ',],
David James2b2e2c52014-12-02 19:32:07 -080047 'TRY': ['T ', 'Trybot Ready ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080048 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040049}
50
51# Order is important -- matches the web ui. This also controls the short
52# entries that we summarize in non-verbose mode.
53GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
54
55
56def red(s):
57 return COLOR.Color(terminal.Color.RED, s)
58
59
60def green(s):
61 return COLOR.Color(terminal.Color.GREEN, s)
62
63
64def blue(s):
65 return COLOR.Color(terminal.Color.BLUE, s)
66
67
68def limits(cls):
69 """Given a dict of fields, calculate the longest string lengths
70
71 This allows you to easily format the output of many results so that the
72 various cols all line up correctly.
73 """
74 lims = {}
75 for cl in cls:
76 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040077 # Use %s rather than str() to avoid codec issues.
78 # We also do this so we can format integers.
79 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040080 return lims
81
82
Mike Frysinger88f27292014-06-17 09:40:45 -070083# TODO: This func really needs to be merged into the core gerrit logic.
84def GetGerrit(opts, cl=None):
85 """Auto pick the right gerrit instance based on the |cl|
86
87 Args:
88 opts: The general options object.
89 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
90
91 Returns:
92 A tuple of a gerrit object and a sanitized CL #.
93 """
94 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -070095 if cl is not None:
Mike Frysinger88f27292014-06-17 09:40:45 -070096 if cl.startswith('*'):
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -070097 gob = site_config.params.INTERNAL_GOB_INSTANCE
Mike Frysinger88f27292014-06-17 09:40:45 -070098 cl = cl[1:]
99 elif ':' in cl:
100 gob, cl = cl.split(':', 1)
101
102 if not gob in opts.gerrit:
103 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
104
105 return (opts.gerrit[gob], cl)
106
107
Mike Frysinger13f23a42013-05-13 17:32:01 -0400108def GetApprovalSummary(_opts, cls):
109 """Return a dict of the most important approvals"""
110 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
111 if 'approvals' in cls['currentPatchSet']:
112 for approver in cls['currentPatchSet']['approvals']:
113 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
114 if not cats:
Ralph Nathan446aee92015-03-23 14:44:56 -0700115 logging.warning('unknown gerrit approval type: %s', approver['type'])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400116 continue
117 cat = cats[0].strip()
118 val = int(approver['value'])
119 if not cat in approvs:
120 # Ignore the extended categories in the summary view.
121 continue
Mike Frysingera0313d02017-07-10 16:44:43 -0400122 elif approvs[cat] == '':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400123 approvs[cat] = val
124 elif val < 0:
125 approvs[cat] = min(approvs[cat], val)
126 else:
127 approvs[cat] = max(approvs[cat], val)
128 return approvs
129
130
Mike Frysingera1b4b272017-04-05 16:11:00 -0400131def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400132 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400133 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400134 lims = {'url': 0, 'project': 0}
135
136 status = ''
137 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400138 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400139 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400140 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400141 functor = lambda x: x
142 elif approvs[cat] < 0:
143 functor = red
144 else:
145 functor = green
146 status += functor('%s:%2s ' % (cat, approvs[cat]))
147
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400148 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
149 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400150
151 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400152 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400153 functor = red if int(approver['value']) < 0 else green
154 n = functor('%2s' % approver['value'])
155 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
156 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500157 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400158
159
Mike Frysingera1b4b272017-04-05 16:11:00 -0400160def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400161 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400162 if opts.raw:
163 pfx = ''
164 # Special case internal Chrome GoB as that is what most devs use.
165 # They can always redirect the list elsewhere via the -g option.
166 if opts.gob == site_config.params.INTERNAL_GOB_INSTANCE:
167 pfx = site_config.params.INTERNAL_CHANGE_PREFIX
168 for cl in cls:
169 print('%s%s' % (pfx, cl['number']))
170
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400171 elif opts.json:
172 json.dump(cls, sys.stdout)
173
Mike Frysingera1b4b272017-04-05 16:11:00 -0400174 else:
175 if lims is None:
176 lims = limits(cls)
177
178 for cl in cls:
179 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
180
181
Mike Frysinger13f23a42013-05-13 17:32:01 -0400182def _MyUserInfo():
Mike Frysinger2cd56022017-01-12 20:56:27 -0500183 """Try to return e-mail addresses used by the active user."""
184 return [git.GetProjectUserEmail(constants.CHROMITE_DIR)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400185
186
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400187def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700188 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800189 if opts.branch is not None:
190 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800191 if opts.project is not None:
192 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800193 if opts.topic is not None:
194 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800195
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400196 if helper is None:
197 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700198 return helper.Query(query, raw=raw, bypass_cache=False)
199
200
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400201def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700202 """Query gerrit and filter/clean up the results"""
203 ret = []
204
Mike Frysinger2cd56022017-01-12 20:56:27 -0500205 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400206 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400207 # Gerrit likes to return a stats record too.
208 if not 'project' in cl:
209 continue
210
211 # Strip off common leading names since the result is still
212 # unique over the whole tree.
213 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400214 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
215 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400216 if cl['project'].startswith('%s/' % pfx):
217 cl['project'] = cl['project'][len(pfx) + 1:]
218
Mike Frysinger479f1192017-09-14 22:36:30 -0400219 cl['url'] = uri_lib.ShortenUri(cl['url'])
220
Mike Frysinger13f23a42013-05-13 17:32:01 -0400221 ret.append(cl)
222
Mike Frysingerb62313a2017-06-30 16:38:58 -0400223 if opts.sort == 'unsorted':
224 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700225 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400226 key = lambda x: int(x[opts.sort])
227 else:
228 key = lambda x: x[opts.sort]
229 return sorted(ret, key=key)
230
231
Mike Frysinger13f23a42013-05-13 17:32:01 -0400232def IsApprover(cl, users):
233 """See if the approvers in |cl| is listed in |users|"""
234 # See if we are listed in the approvals list. We have to parse
235 # this by hand as the gerrit query system doesn't support it :(
236 # http://code.google.com/p/gerrit/issues/detail?id=1235
237 if 'approvals' not in cl['currentPatchSet']:
238 return False
239
240 if isinstance(users, basestring):
241 users = (users,)
242
243 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700244 if (approver['by']['email'] in users and
245 approver['type'] == 'CRVW' and
246 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400247 return True
248
249 return False
250
251
252def UserActTodo(opts):
253 """List CLs needing your review"""
Mike Frysinger2cd56022017-01-12 20:56:27 -0500254 emails = _MyUserInfo()
255 cls = FilteredQuery(opts, 'reviewer:self status:open NOT owner:self')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400256 cls = [x for x in cls if not IsApprover(x, emails)]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400257 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400258
259
Mike Frysingera1db2c42014-06-15 00:42:48 -0700260def UserActSearch(opts, query):
261 """List CLs matching the Gerrit <search query>"""
262 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400263 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400264
265
Mike Frysingera1db2c42014-06-15 00:42:48 -0700266def UserActMine(opts):
267 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700268 if opts.draft:
269 rule = 'is:draft'
270 else:
271 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500272 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700273
274
Paul Hobbs89765232015-06-24 14:07:49 -0700275def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
276 """Runs breadth first search starting from the nodes in |to_visit|
277
278 Args:
279 to_visit: the starting nodes
280 children: a function which takes a node and returns the nodes adjacent to it
281 visited_key: a function for deduplicating node visits. Defaults to the
282 identity function (lambda x: x)
283
284 Returns:
285 A list of nodes which are reachable from any node in |to_visit| by calling
286 |children| any number of times.
287 """
288 to_visit = list(to_visit)
289 seen = set(map(visited_key, to_visit))
290 for node in to_visit:
291 for child in children(node):
292 key = visited_key(child)
293 if key not in seen:
294 seen.add(key)
295 to_visit.append(child)
296 return to_visit
297
298
299def UserActDeps(opts, query):
300 """List CLs matching a query, and all transitive dependencies of those CLs"""
301 cls = _Query(opts, query, raw=False)
302
303 @cros_build_lib.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400304 def _QueryChange(cl, helper=None):
305 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700306
Mike Frysinger5726da92017-09-20 22:14:25 -0400307 def _ProcessDeps(cl, deps, required):
308 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700309 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400310 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400311 if not dep.remote in opts.gerrit:
312 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
313 remote=dep.remote, print_cmd=opts.debug)
314 helper = opts.gerrit[dep.remote]
315
Paul Hobbs89765232015-06-24 14:07:49 -0700316 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400317 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
318
319 # Handle empty results. If we found a commit that was pushed directly
320 # (e.g. a bot commit), then gerrit won't know about it.
321 if not changes:
322 if required:
323 logging.error('CL %s depends on %s which cannot be found',
324 cl, dep.ToGerritQueryText())
325 continue
326
327 # Our query might have matched more than one result. This can come up
328 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
329 # across multiple repos/branches. We blindly check all of them in the
330 # hopes that all open ones are what the user wants, but then again the
331 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
332 if len(changes) > 1:
333 logging.warning('CL %s has an ambiguous CQ dependency %s',
334 cl, dep.ToGerritQueryText())
335 for change in changes:
336 if change.status == 'NEW':
337 yield change
338
339 def _Children(cl):
340 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
341 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
342 yield change
343 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
344 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700345
346 transitives = _BreadthFirstSearch(
347 cls, _Children,
348 visited_key=lambda cl: cl.gerrit_number)
349
350 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400351 PrintCls(opts, transitives_raw)
Paul Hobbs89765232015-06-24 14:07:49 -0700352
353
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700354def UserActInspect(opts, *args):
355 """Inspect CL number <n> [n ...]"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400356 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700357 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400358 helper, cl = GetGerrit(opts, arg)
359 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
360 if change:
361 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700362 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400363 logging.warning('no results found for CL %s', arg)
364 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400365
366
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700367def UserActReview(opts, *args):
368 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
369 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700370 for arg in args[:-1]:
371 helper, cl = GetGerrit(opts, arg)
372 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700373UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400374
375
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700376def UserActVerify(opts, *args):
377 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
378 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700379 for arg in args[:-1]:
380 helper, cl = GetGerrit(opts, arg)
381 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700382UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400383
384
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700385def UserActReady(opts, *args):
Kirtika Ruchandanica852f42017-05-23 18:18:05 -0700386 """Mark CL <n> [n ...] with ready status <0,1>"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700387 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700388 for arg in args[:-1]:
389 helper, cl = GetGerrit(opts, arg)
390 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700391UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400392
393
Mike Frysinger15b23e42014-12-05 17:00:05 -0500394def UserActTrybotready(opts, *args):
395 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
396 num = args[-1]
397 for arg in args[:-1]:
398 helper, cl = GetGerrit(opts, arg)
399 helper.SetReview(cl, labels={'Trybot-Ready': num}, dryrun=opts.dryrun)
400UserActTrybotready.arg_min = 2
401
402
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700403def UserActSubmit(opts, *args):
404 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700405 for arg in args:
406 helper, cl = GetGerrit(opts, arg)
407 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400408
409
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700410def UserActAbandon(opts, *args):
411 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700412 for arg in args:
413 helper, cl = GetGerrit(opts, arg)
414 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400415
416
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700417def UserActRestore(opts, *args):
418 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700419 for arg in args:
420 helper, cl = GetGerrit(opts, arg)
421 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400422
423
Mike Frysinger88f27292014-06-17 09:40:45 -0700424def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700425 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500426 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700427 # Allow for optional leading '~'.
428 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
429 add_list, remove_list, invalid_list = [], [], []
430
431 for x in emails:
432 if not email_validator.match(x):
433 invalid_list.append(x)
434 elif x[0] == '~':
435 remove_list.append(x[1:])
436 else:
437 add_list.append(x)
438
439 if invalid_list:
440 cros_build_lib.Die(
441 'Invalid email address(es): %s' % ', '.join(invalid_list))
442
443 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700444 helper, cl = GetGerrit(opts, cl)
445 helper.SetReviewers(cl, add=add_list, remove=remove_list,
446 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700447
448
Allen Li38abdaa2017-03-16 13:25:02 -0700449def UserActAssign(opts, cl, assignee):
450 """Set assignee for CL <n>"""
451 helper, cl = GetGerrit(opts, cl)
452 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
453
454
Mike Frysinger88f27292014-06-17 09:40:45 -0700455def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530456 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700457 helper, cl = GetGerrit(opts, cl)
458 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530459
460
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800461def UserActTopic(opts, topic, *args):
462 """Set |topic| for CL number <n> [n ...]"""
463 for arg in args:
464 helper, arg = GetGerrit(opts, arg)
465 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
466
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700467def UserActPrivate(opts, cl, private_str):
468 """Set private bit on CL to private"""
469 try:
470 private = cros_build_lib.BooleanShellValue(private_str, False)
471 except ValueError:
472 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
473
474 helper, cl = GetGerrit(opts, cl)
475 helper.SetPrivate(cl, private)
476
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800477
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800478def UserActSethashtags(opts, cl, *args):
479 """Add/remove hashtags for CL <n> (prepend with '~' to remove)"""
480 hashtags = args
481 add = []
482 remove = []
483 for hashtag in hashtags:
484 if hashtag.startswith('~'):
485 remove.append(hashtag[1:])
486 else:
487 add.append(hashtag)
488 helper, cl = GetGerrit(opts, cl)
489 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
490
491
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700492def UserActDeletedraft(opts, *args):
Marc Herbert02448c82015-10-07 14:03:34 -0700493 """Delete draft CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700494 for arg in args:
495 helper, cl = GetGerrit(opts, arg)
496 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800497
498
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800499def UserActAccount(opts):
500 """Get user account information."""
501 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400502 acct = helper.GetAccount()
503 if opts.json:
504 json.dump(acct, sys.stdout)
505 else:
506 print('account_id:%i %s <%s>' %
507 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800508
509
Mike Frysinger108eda22018-06-06 18:45:12 -0400510def GetParser():
511 """Returns the parser to use for this module."""
512 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400513
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500514 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400515
516There is no support for doing line-by-line code review via the command line.
517This helps you manage various bits and CL status.
518
Mike Frysingera1db2c42014-06-15 00:42:48 -0700519For general Gerrit documentation, see:
520 https://gerrit-review.googlesource.com/Documentation/
521The Searching Changes page covers the search query syntax:
522 https://gerrit-review.googlesource.com/Documentation/user-search.html
523
Mike Frysinger13f23a42013-05-13 17:32:01 -0400524Example:
525 $ gerrit todo # List all the CLs that await your review.
526 $ gerrit mine # List all of your open CLs.
527 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
528 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
529 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700530Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700531 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
532ready.
533 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
534ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400535 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400536
537Actions:"""
Mike Frysinger108eda22018-06-06 18:45:12 -0400538 indent = max([len(x) - len(ACTION_PREFIX) for x in actions])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400539 for a in sorted(actions):
Mike Frysinger108eda22018-06-06 18:45:12 -0400540 cmd = a[len(ACTION_PREFIX):]
Mike Frysinger15b23e42014-12-05 17:00:05 -0500541 # Sanity check for devs adding new commands. Should be quick.
542 if cmd != cmd.lower().capitalize():
543 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
544 (cmd, cmd.lower().capitalize()))
545 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400546
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500547 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500548 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700549 default=site_config.params.EXTERNAL_GOB_INSTANCE,
550 const=site_config.params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500551 help='Query internal Chromium Gerrit instance')
552 parser.add_argument('-g', '--gob',
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700553 default=site_config.params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500554 help=('Gerrit (on borg) instance to query (default: %s)' %
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700555 (site_config.params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500556 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400557 help='Key to sort on (number, project); use "unsorted" '
558 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700559 parser.add_argument('--raw', default=False, action='store_true',
560 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400561 parser.add_argument('--json', default=False, action='store_true',
562 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700563 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
564 dest='dryrun',
565 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500566 parser.add_argument('-v', '--verbose', default=False, action='store_true',
567 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800568 parser.add_argument('-b', '--branch',
569 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700570 parser.add_argument('--draft', default=False, action='store_true',
571 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800572 parser.add_argument('-p', '--project',
573 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800574 parser.add_argument('-t', '--topic',
575 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500576 parser.add_argument('action', help='The gerrit action to perform')
577 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400578
579 return parser
580
581
582def main(argv):
583 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500584 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400585
Mike Frysinger88f27292014-06-17 09:40:45 -0700586 # A cache of gerrit helpers we'll load on demand.
587 opts.gerrit = {}
588 opts.Freeze()
589
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400590 # pylint: disable=W0603
591 global COLOR
592 COLOR = terminal.Color(enabled=opts.color)
593
Mike Frysinger13f23a42013-05-13 17:32:01 -0400594 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400595 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400596 if functor:
597 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700598 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700599 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500600 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700601 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500602 (opts.action, arg_min))
603 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400604 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500605 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700606 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500607 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500608 except (cros_build_lib.RunCommandError, gerrit.GerritException,
609 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700610 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400611 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500612 parser.error('unknown action: %s' % (opts.action,))