blob: c7f4c27f1594db52b887381dfc769aa72abf1467 [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 Frysinger10666292018-07-12 01:03:38 -040028from chromite.lib import memoize
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
Mike Frysinger13f23a42013-05-13 17:32:01 -040031
32
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -070033site_config = config_lib.GetConfig()
34
35
Mike Frysinger108eda22018-06-06 18:45:12 -040036# Locate actions that are exposed to the user. All functions that start
37# with "UserAct" are fair game.
38ACTION_PREFIX = 'UserAct'
39
40
Mike Frysinger031ad0b2013-05-14 18:15:34 -040041COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040042
43# Map the internal names to the ones we normally show on the web ui.
44GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080045 'COMR': ['CQ', 'Commit Queue ',],
46 'CRVW': ['CR', 'Code Review ',],
47 'SUBM': ['S ', 'Submitted ',],
David James2b2e2c52014-12-02 19:32:07 -080048 'TRY': ['T ', 'Trybot Ready ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080049 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040050}
51
52# Order is important -- matches the web ui. This also controls the short
53# entries that we summarize in non-verbose mode.
54GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
55
56
57def red(s):
58 return COLOR.Color(terminal.Color.RED, s)
59
60
61def green(s):
62 return COLOR.Color(terminal.Color.GREEN, s)
63
64
65def blue(s):
66 return COLOR.Color(terminal.Color.BLUE, s)
67
68
69def limits(cls):
70 """Given a dict of fields, calculate the longest string lengths
71
72 This allows you to easily format the output of many results so that the
73 various cols all line up correctly.
74 """
75 lims = {}
76 for cl in cls:
77 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040078 # Use %s rather than str() to avoid codec issues.
79 # We also do this so we can format integers.
80 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040081 return lims
82
83
Mike Frysinger88f27292014-06-17 09:40:45 -070084# TODO: This func really needs to be merged into the core gerrit logic.
85def GetGerrit(opts, cl=None):
86 """Auto pick the right gerrit instance based on the |cl|
87
88 Args:
89 opts: The general options object.
90 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
91
92 Returns:
93 A tuple of a gerrit object and a sanitized CL #.
94 """
95 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -070096 if cl is not None:
Mike Frysinger88f27292014-06-17 09:40:45 -070097 if cl.startswith('*'):
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -070098 gob = site_config.params.INTERNAL_GOB_INSTANCE
Mike Frysinger88f27292014-06-17 09:40:45 -070099 cl = cl[1:]
100 elif ':' in cl:
101 gob, cl = cl.split(':', 1)
102
103 if not gob in opts.gerrit:
104 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
105
106 return (opts.gerrit[gob], cl)
107
108
Mike Frysinger13f23a42013-05-13 17:32:01 -0400109def GetApprovalSummary(_opts, cls):
110 """Return a dict of the most important approvals"""
111 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
112 if 'approvals' in cls['currentPatchSet']:
113 for approver in cls['currentPatchSet']['approvals']:
114 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
115 if not cats:
Ralph Nathan446aee92015-03-23 14:44:56 -0700116 logging.warning('unknown gerrit approval type: %s', approver['type'])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400117 continue
118 cat = cats[0].strip()
119 val = int(approver['value'])
120 if not cat in approvs:
121 # Ignore the extended categories in the summary view.
122 continue
Mike Frysingera0313d02017-07-10 16:44:43 -0400123 elif approvs[cat] == '':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400124 approvs[cat] = val
125 elif val < 0:
126 approvs[cat] = min(approvs[cat], val)
127 else:
128 approvs[cat] = max(approvs[cat], val)
129 return approvs
130
131
Mike Frysingera1b4b272017-04-05 16:11:00 -0400132def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400133 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400134 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400135 lims = {'url': 0, 'project': 0}
136
137 status = ''
138 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400139 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400140 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400141 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400142 functor = lambda x: x
143 elif approvs[cat] < 0:
144 functor = red
145 else:
146 functor = green
147 status += functor('%s:%2s ' % (cat, approvs[cat]))
148
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400149 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
150 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400151
152 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400153 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400154 functor = red if int(approver['value']) < 0 else green
155 n = functor('%2s' % approver['value'])
156 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
157 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500158 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400159
160
Mike Frysingera1b4b272017-04-05 16:11:00 -0400161def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400162 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400163 if opts.raw:
164 pfx = ''
165 # Special case internal Chrome GoB as that is what most devs use.
166 # They can always redirect the list elsewhere via the -g option.
167 if opts.gob == site_config.params.INTERNAL_GOB_INSTANCE:
168 pfx = site_config.params.INTERNAL_CHANGE_PREFIX
169 for cl in cls:
170 print('%s%s' % (pfx, cl['number']))
171
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400172 elif opts.json:
173 json.dump(cls, sys.stdout)
174
Mike Frysingera1b4b272017-04-05 16:11:00 -0400175 else:
176 if lims is None:
177 lims = limits(cls)
178
179 for cl in cls:
180 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
181
182
Mike Frysinger13f23a42013-05-13 17:32:01 -0400183def _MyUserInfo():
Mike Frysinger2cd56022017-01-12 20:56:27 -0500184 """Try to return e-mail addresses used by the active user."""
185 return [git.GetProjectUserEmail(constants.CHROMITE_DIR)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400186
187
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400188def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700189 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800190 if opts.branch is not None:
191 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800192 if opts.project is not None:
193 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800194 if opts.topic is not None:
195 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800196
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400197 if helper is None:
198 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700199 return helper.Query(query, raw=raw, bypass_cache=False)
200
201
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400202def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700203 """Query gerrit and filter/clean up the results"""
204 ret = []
205
Mike Frysinger2cd56022017-01-12 20:56:27 -0500206 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400207 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400208 # Gerrit likes to return a stats record too.
209 if not 'project' in cl:
210 continue
211
212 # Strip off common leading names since the result is still
213 # unique over the whole tree.
214 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400215 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
216 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400217 if cl['project'].startswith('%s/' % pfx):
218 cl['project'] = cl['project'][len(pfx) + 1:]
219
Mike Frysinger479f1192017-09-14 22:36:30 -0400220 cl['url'] = uri_lib.ShortenUri(cl['url'])
221
Mike Frysinger13f23a42013-05-13 17:32:01 -0400222 ret.append(cl)
223
Mike Frysingerb62313a2017-06-30 16:38:58 -0400224 if opts.sort == 'unsorted':
225 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700226 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400227 key = lambda x: int(x[opts.sort])
228 else:
229 key = lambda x: x[opts.sort]
230 return sorted(ret, key=key)
231
232
Mike Frysinger13f23a42013-05-13 17:32:01 -0400233def IsApprover(cl, users):
234 """See if the approvers in |cl| is listed in |users|"""
235 # See if we are listed in the approvals list. We have to parse
236 # this by hand as the gerrit query system doesn't support it :(
237 # http://code.google.com/p/gerrit/issues/detail?id=1235
238 if 'approvals' not in cl['currentPatchSet']:
239 return False
240
241 if isinstance(users, basestring):
242 users = (users,)
243
244 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700245 if (approver['by']['email'] in users and
246 approver['type'] == 'CRVW' and
247 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400248 return True
249
250 return False
251
252
253def UserActTodo(opts):
254 """List CLs needing your review"""
Mike Frysinger2cd56022017-01-12 20:56:27 -0500255 emails = _MyUserInfo()
256 cls = FilteredQuery(opts, 'reviewer:self status:open NOT owner:self')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400257 cls = [x for x in cls if not IsApprover(x, emails)]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400258 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400259
260
Mike Frysingera1db2c42014-06-15 00:42:48 -0700261def UserActSearch(opts, query):
262 """List CLs matching the Gerrit <search query>"""
263 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400264 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400265
266
Mike Frysingera1db2c42014-06-15 00:42:48 -0700267def UserActMine(opts):
268 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700269 if opts.draft:
270 rule = 'is:draft'
271 else:
272 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500273 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700274
275
Paul Hobbs89765232015-06-24 14:07:49 -0700276def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
277 """Runs breadth first search starting from the nodes in |to_visit|
278
279 Args:
280 to_visit: the starting nodes
281 children: a function which takes a node and returns the nodes adjacent to it
282 visited_key: a function for deduplicating node visits. Defaults to the
283 identity function (lambda x: x)
284
285 Returns:
286 A list of nodes which are reachable from any node in |to_visit| by calling
287 |children| any number of times.
288 """
289 to_visit = list(to_visit)
290 seen = set(map(visited_key, to_visit))
291 for node in to_visit:
292 for child in children(node):
293 key = visited_key(child)
294 if key not in seen:
295 seen.add(key)
296 to_visit.append(child)
297 return to_visit
298
299
300def UserActDeps(opts, query):
301 """List CLs matching a query, and all transitive dependencies of those CLs"""
302 cls = _Query(opts, query, raw=False)
303
Mike Frysinger10666292018-07-12 01:03:38 -0400304 @memoize.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400305 def _QueryChange(cl, helper=None):
306 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700307
Mike Frysinger5726da92017-09-20 22:14:25 -0400308 def _ProcessDeps(cl, deps, required):
309 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700310 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400311 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400312 if not dep.remote in opts.gerrit:
313 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
314 remote=dep.remote, print_cmd=opts.debug)
315 helper = opts.gerrit[dep.remote]
316
Paul Hobbs89765232015-06-24 14:07:49 -0700317 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400318 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
319
320 # Handle empty results. If we found a commit that was pushed directly
321 # (e.g. a bot commit), then gerrit won't know about it.
322 if not changes:
323 if required:
324 logging.error('CL %s depends on %s which cannot be found',
325 cl, dep.ToGerritQueryText())
326 continue
327
328 # Our query might have matched more than one result. This can come up
329 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
330 # across multiple repos/branches. We blindly check all of them in the
331 # hopes that all open ones are what the user wants, but then again the
332 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
333 if len(changes) > 1:
334 logging.warning('CL %s has an ambiguous CQ dependency %s',
335 cl, dep.ToGerritQueryText())
336 for change in changes:
337 if change.status == 'NEW':
338 yield change
339
340 def _Children(cl):
341 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
342 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
343 yield change
344 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
345 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700346
347 transitives = _BreadthFirstSearch(
348 cls, _Children,
349 visited_key=lambda cl: cl.gerrit_number)
350
351 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400352 PrintCls(opts, transitives_raw)
Paul Hobbs89765232015-06-24 14:07:49 -0700353
354
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700355def UserActInspect(opts, *args):
356 """Inspect CL number <n> [n ...]"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400357 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700358 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400359 helper, cl = GetGerrit(opts, arg)
360 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
361 if change:
362 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700363 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400364 logging.warning('no results found for CL %s', arg)
365 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400366
367
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700368def UserActReview(opts, *args):
369 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
370 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700371 for arg in args[:-1]:
372 helper, cl = GetGerrit(opts, arg)
373 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700374UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400375
376
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700377def UserActVerify(opts, *args):
378 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
379 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700380 for arg in args[:-1]:
381 helper, cl = GetGerrit(opts, arg)
382 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700383UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400384
385
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700386def UserActReady(opts, *args):
Kirtika Ruchandanica852f42017-05-23 18:18:05 -0700387 """Mark CL <n> [n ...] with ready status <0,1>"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700388 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700389 for arg in args[:-1]:
390 helper, cl = GetGerrit(opts, arg)
391 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700392UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400393
394
Mike Frysinger15b23e42014-12-05 17:00:05 -0500395def UserActTrybotready(opts, *args):
396 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
397 num = args[-1]
398 for arg in args[:-1]:
399 helper, cl = GetGerrit(opts, arg)
400 helper.SetReview(cl, labels={'Trybot-Ready': num}, dryrun=opts.dryrun)
401UserActTrybotready.arg_min = 2
402
403
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700404def UserActSubmit(opts, *args):
405 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700406 for arg in args:
407 helper, cl = GetGerrit(opts, arg)
408 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400409
410
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700411def UserActAbandon(opts, *args):
412 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700413 for arg in args:
414 helper, cl = GetGerrit(opts, arg)
415 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400416
417
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700418def UserActRestore(opts, *args):
419 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700420 for arg in args:
421 helper, cl = GetGerrit(opts, arg)
422 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400423
424
Mike Frysinger88f27292014-06-17 09:40:45 -0700425def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700426 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500427 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700428 # Allow for optional leading '~'.
429 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
430 add_list, remove_list, invalid_list = [], [], []
431
432 for x in emails:
433 if not email_validator.match(x):
434 invalid_list.append(x)
435 elif x[0] == '~':
436 remove_list.append(x[1:])
437 else:
438 add_list.append(x)
439
440 if invalid_list:
441 cros_build_lib.Die(
442 'Invalid email address(es): %s' % ', '.join(invalid_list))
443
444 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700445 helper, cl = GetGerrit(opts, cl)
446 helper.SetReviewers(cl, add=add_list, remove=remove_list,
447 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700448
449
Allen Li38abdaa2017-03-16 13:25:02 -0700450def UserActAssign(opts, cl, assignee):
451 """Set assignee for CL <n>"""
452 helper, cl = GetGerrit(opts, cl)
453 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
454
455
Mike Frysinger88f27292014-06-17 09:40:45 -0700456def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530457 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700458 helper, cl = GetGerrit(opts, cl)
459 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530460
461
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800462def UserActTopic(opts, topic, *args):
463 """Set |topic| for CL number <n> [n ...]"""
464 for arg in args:
465 helper, arg = GetGerrit(opts, arg)
466 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
467
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700468def UserActPrivate(opts, cl, private_str):
469 """Set private bit on CL to private"""
470 try:
471 private = cros_build_lib.BooleanShellValue(private_str, False)
472 except ValueError:
473 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
474
475 helper, cl = GetGerrit(opts, cl)
476 helper.SetPrivate(cl, private)
477
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800478
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800479def UserActSethashtags(opts, cl, *args):
480 """Add/remove hashtags for CL <n> (prepend with '~' to remove)"""
481 hashtags = args
482 add = []
483 remove = []
484 for hashtag in hashtags:
485 if hashtag.startswith('~'):
486 remove.append(hashtag[1:])
487 else:
488 add.append(hashtag)
489 helper, cl = GetGerrit(opts, cl)
490 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
491
492
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700493def UserActDeletedraft(opts, *args):
Marc Herbert02448c82015-10-07 14:03:34 -0700494 """Delete draft CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700495 for arg in args:
496 helper, cl = GetGerrit(opts, arg)
497 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800498
499
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800500def UserActAccount(opts):
501 """Get user account information."""
502 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400503 acct = helper.GetAccount()
504 if opts.json:
505 json.dump(acct, sys.stdout)
506 else:
507 print('account_id:%i %s <%s>' %
508 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800509
510
Mike Frysinger108eda22018-06-06 18:45:12 -0400511def GetParser():
512 """Returns the parser to use for this module."""
513 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400514
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500515 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400516
517There is no support for doing line-by-line code review via the command line.
518This helps you manage various bits and CL status.
519
Mike Frysingera1db2c42014-06-15 00:42:48 -0700520For general Gerrit documentation, see:
521 https://gerrit-review.googlesource.com/Documentation/
522The Searching Changes page covers the search query syntax:
523 https://gerrit-review.googlesource.com/Documentation/user-search.html
524
Mike Frysinger13f23a42013-05-13 17:32:01 -0400525Example:
526 $ gerrit todo # List all the CLs that await your review.
527 $ gerrit mine # List all of your open CLs.
528 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
529 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
530 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700531Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700532 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
533ready.
534 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
535ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400536 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400537
538Actions:"""
Mike Frysinger108eda22018-06-06 18:45:12 -0400539 indent = max([len(x) - len(ACTION_PREFIX) for x in actions])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400540 for a in sorted(actions):
Mike Frysinger108eda22018-06-06 18:45:12 -0400541 cmd = a[len(ACTION_PREFIX):]
Mike Frysinger15b23e42014-12-05 17:00:05 -0500542 # Sanity check for devs adding new commands. Should be quick.
543 if cmd != cmd.lower().capitalize():
544 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
545 (cmd, cmd.lower().capitalize()))
546 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400547
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500548 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500549 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700550 default=site_config.params.EXTERNAL_GOB_INSTANCE,
551 const=site_config.params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500552 help='Query internal Chromium Gerrit instance')
553 parser.add_argument('-g', '--gob',
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700554 default=site_config.params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500555 help=('Gerrit (on borg) instance to query (default: %s)' %
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700556 (site_config.params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500557 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400558 help='Key to sort on (number, project); use "unsorted" '
559 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700560 parser.add_argument('--raw', default=False, action='store_true',
561 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400562 parser.add_argument('--json', default=False, action='store_true',
563 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700564 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
565 dest='dryrun',
566 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500567 parser.add_argument('-v', '--verbose', default=False, action='store_true',
568 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800569 parser.add_argument('-b', '--branch',
570 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700571 parser.add_argument('--draft', default=False, action='store_true',
572 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800573 parser.add_argument('-p', '--project',
574 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800575 parser.add_argument('-t', '--topic',
576 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500577 parser.add_argument('action', help='The gerrit action to perform')
578 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400579
580 return parser
581
582
583def main(argv):
584 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500585 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400586
Mike Frysinger88f27292014-06-17 09:40:45 -0700587 # A cache of gerrit helpers we'll load on demand.
588 opts.gerrit = {}
589 opts.Freeze()
590
Mike Frysinger27e21b72018-07-12 14:20:21 -0400591 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400592 global COLOR
593 COLOR = terminal.Color(enabled=opts.color)
594
Mike Frysinger13f23a42013-05-13 17:32:01 -0400595 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400596 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400597 if functor:
598 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700599 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700600 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500601 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700602 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500603 (opts.action, arg_min))
604 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400605 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500606 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700607 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500608 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500609 except (cros_build_lib.RunCommandError, gerrit.GerritException,
610 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700611 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400612 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500613 parser.error('unknown action: %s' % (opts.action,))