blob: 16c00e4bb841d5acea04d989edeaa1b96232d930 [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
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 Frysinger031ad0b2013-05-14 18:15:34 -040038COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040039
40# Map the internal names to the ones we normally show on the web ui.
41GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080042 'COMR': ['CQ', 'Commit Queue ',],
43 'CRVW': ['CR', 'Code Review ',],
44 'SUBM': ['S ', 'Submitted ',],
David James2b2e2c52014-12-02 19:32:07 -080045 'TRY': ['T ', 'Trybot Ready ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080046 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040047}
48
49# Order is important -- matches the web ui. This also controls the short
50# entries that we summarize in non-verbose mode.
51GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
52
53
54def red(s):
55 return COLOR.Color(terminal.Color.RED, s)
56
57
58def green(s):
59 return COLOR.Color(terminal.Color.GREEN, s)
60
61
62def blue(s):
63 return COLOR.Color(terminal.Color.BLUE, s)
64
65
66def limits(cls):
67 """Given a dict of fields, calculate the longest string lengths
68
69 This allows you to easily format the output of many results so that the
70 various cols all line up correctly.
71 """
72 lims = {}
73 for cl in cls:
74 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040075 # Use %s rather than str() to avoid codec issues.
76 # We also do this so we can format integers.
77 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040078 return lims
79
80
Mike Frysinger88f27292014-06-17 09:40:45 -070081# TODO: This func really needs to be merged into the core gerrit logic.
82def GetGerrit(opts, cl=None):
83 """Auto pick the right gerrit instance based on the |cl|
84
85 Args:
86 opts: The general options object.
87 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
88
89 Returns:
90 A tuple of a gerrit object and a sanitized CL #.
91 """
92 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -070093 if cl is not None:
Mike Frysinger88f27292014-06-17 09:40:45 -070094 if cl.startswith('*'):
Alex Klein2ab29cc2018-07-19 12:01:00 -060095 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Mike Frysinger88f27292014-06-17 09:40:45 -070096 cl = cl[1:]
97 elif ':' in cl:
98 gob, cl = cl.split(':', 1)
99
100 if not gob in opts.gerrit:
101 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
102
103 return (opts.gerrit[gob], cl)
104
105
Mike Frysinger13f23a42013-05-13 17:32:01 -0400106def GetApprovalSummary(_opts, cls):
107 """Return a dict of the most important approvals"""
108 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700109 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
110 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
111 if not cats:
112 logging.warning('unknown gerrit approval type: %s', approver['type'])
113 continue
114 cat = cats[0].strip()
115 val = int(approver['value'])
116 if not cat in approvs:
117 # Ignore the extended categories in the summary view.
118 continue
119 elif approvs[cat] == '':
120 approvs[cat] = val
121 elif val < 0:
122 approvs[cat] = min(approvs[cat], val)
123 else:
124 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400125 return approvs
126
127
Mike Frysingera1b4b272017-04-05 16:11:00 -0400128def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400129 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400130 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400131 lims = {'url': 0, 'project': 0}
132
133 status = ''
134 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400135 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400136 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400137 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400138 functor = lambda x: x
139 elif approvs[cat] < 0:
140 functor = red
141 else:
142 functor = green
143 status += functor('%s:%2s ' % (cat, approvs[cat]))
144
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400145 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
146 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400147
148 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400149 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400150 functor = red if int(approver['value']) < 0 else green
151 n = functor('%2s' % approver['value'])
152 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
153 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500154 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400155
156
Mike Frysingera1b4b272017-04-05 16:11:00 -0400157def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400158 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400159 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600160 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400161 pfx = ''
162 # Special case internal Chrome GoB as that is what most devs use.
163 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600164 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
165 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400166 for cl in cls:
167 print('%s%s' % (pfx, cl['number']))
168
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400169 elif opts.json:
170 json.dump(cls, sys.stdout)
171
Mike Frysingera1b4b272017-04-05 16:11:00 -0400172 else:
173 if lims is None:
174 lims = limits(cls)
175
176 for cl in cls:
177 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
178
179
Mike Frysinger13f23a42013-05-13 17:32:01 -0400180def _MyUserInfo():
Mike Frysinger2cd56022017-01-12 20:56:27 -0500181 """Try to return e-mail addresses used by the active user."""
182 return [git.GetProjectUserEmail(constants.CHROMITE_DIR)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400183
184
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400185def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700186 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800187 if opts.branch is not None:
188 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800189 if opts.project is not None:
190 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800191 if opts.topic is not None:
192 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800193
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400194 if helper is None:
195 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700196 return helper.Query(query, raw=raw, bypass_cache=False)
197
198
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400199def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700200 """Query gerrit and filter/clean up the results"""
201 ret = []
202
Mike Frysinger2cd56022017-01-12 20:56:27 -0500203 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400204 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400205 # Gerrit likes to return a stats record too.
206 if not 'project' in cl:
207 continue
208
209 # Strip off common leading names since the result is still
210 # unique over the whole tree.
211 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400212 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
213 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400214 if cl['project'].startswith('%s/' % pfx):
215 cl['project'] = cl['project'][len(pfx) + 1:]
216
Mike Frysinger479f1192017-09-14 22:36:30 -0400217 cl['url'] = uri_lib.ShortenUri(cl['url'])
218
Mike Frysinger13f23a42013-05-13 17:32:01 -0400219 ret.append(cl)
220
Mike Frysingerb62313a2017-06-30 16:38:58 -0400221 if opts.sort == 'unsorted':
222 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700223 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400224 key = lambda x: int(x[opts.sort])
225 else:
226 key = lambda x: x[opts.sort]
227 return sorted(ret, key=key)
228
229
Mike Frysinger13f23a42013-05-13 17:32:01 -0400230def IsApprover(cl, users):
231 """See if the approvers in |cl| is listed in |users|"""
232 # See if we are listed in the approvals list. We have to parse
233 # this by hand as the gerrit query system doesn't support it :(
234 # http://code.google.com/p/gerrit/issues/detail?id=1235
235 if 'approvals' not in cl['currentPatchSet']:
236 return False
237
238 if isinstance(users, basestring):
239 users = (users,)
240
241 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700242 if (approver['by']['email'] in users and
243 approver['type'] == 'CRVW' and
244 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400245 return True
246
247 return False
248
249
250def UserActTodo(opts):
251 """List CLs needing your review"""
Mike Frysinger2cd56022017-01-12 20:56:27 -0500252 emails = _MyUserInfo()
253 cls = FilteredQuery(opts, 'reviewer:self status:open NOT owner:self')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400254 cls = [x for x in cls if not IsApprover(x, emails)]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400255 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400256
257
Mike Frysingera1db2c42014-06-15 00:42:48 -0700258def UserActSearch(opts, query):
259 """List CLs matching the Gerrit <search query>"""
260 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400261 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400262
263
Mike Frysingera1db2c42014-06-15 00:42:48 -0700264def UserActMine(opts):
265 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700266 if opts.draft:
267 rule = 'is:draft'
268 else:
269 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500270 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700271
272
Paul Hobbs89765232015-06-24 14:07:49 -0700273def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
274 """Runs breadth first search starting from the nodes in |to_visit|
275
276 Args:
277 to_visit: the starting nodes
278 children: a function which takes a node and returns the nodes adjacent to it
279 visited_key: a function for deduplicating node visits. Defaults to the
280 identity function (lambda x: x)
281
282 Returns:
283 A list of nodes which are reachable from any node in |to_visit| by calling
284 |children| any number of times.
285 """
286 to_visit = list(to_visit)
287 seen = set(map(visited_key, to_visit))
288 for node in to_visit:
289 for child in children(node):
290 key = visited_key(child)
291 if key not in seen:
292 seen.add(key)
293 to_visit.append(child)
294 return to_visit
295
296
297def UserActDeps(opts, query):
298 """List CLs matching a query, and all transitive dependencies of those CLs"""
299 cls = _Query(opts, query, raw=False)
300
Mike Frysinger10666292018-07-12 01:03:38 -0400301 @memoize.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400302 def _QueryChange(cl, helper=None):
303 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700304
Mike Frysinger5726da92017-09-20 22:14:25 -0400305 def _ProcessDeps(cl, deps, required):
306 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700307 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400308 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400309 if not dep.remote in opts.gerrit:
310 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
311 remote=dep.remote, print_cmd=opts.debug)
312 helper = opts.gerrit[dep.remote]
313
Paul Hobbs89765232015-06-24 14:07:49 -0700314 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400315 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
316
317 # Handle empty results. If we found a commit that was pushed directly
318 # (e.g. a bot commit), then gerrit won't know about it.
319 if not changes:
320 if required:
321 logging.error('CL %s depends on %s which cannot be found',
322 cl, dep.ToGerritQueryText())
323 continue
324
325 # Our query might have matched more than one result. This can come up
326 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
327 # across multiple repos/branches. We blindly check all of them in the
328 # hopes that all open ones are what the user wants, but then again the
329 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
330 if len(changes) > 1:
331 logging.warning('CL %s has an ambiguous CQ dependency %s',
332 cl, dep.ToGerritQueryText())
333 for change in changes:
334 if change.status == 'NEW':
335 yield change
336
337 def _Children(cl):
338 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
339 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
340 yield change
341 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
342 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700343
344 transitives = _BreadthFirstSearch(
345 cls, _Children,
346 visited_key=lambda cl: cl.gerrit_number)
347
348 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400349 PrintCls(opts, transitives_raw)
Paul Hobbs89765232015-06-24 14:07:49 -0700350
351
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700352def UserActInspect(opts, *args):
353 """Inspect CL number <n> [n ...]"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400354 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700355 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400356 helper, cl = GetGerrit(opts, arg)
357 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
358 if change:
359 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700360 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400361 logging.warning('no results found for CL %s', arg)
362 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400363
364
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700365def UserActReview(opts, *args):
366 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
367 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700368 for arg in args[:-1]:
369 helper, cl = GetGerrit(opts, arg)
370 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700371UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400372
373
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700374def UserActVerify(opts, *args):
375 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
376 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700377 for arg in args[:-1]:
378 helper, cl = GetGerrit(opts, arg)
379 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700380UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400381
382
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700383def UserActReady(opts, *args):
Kirtika Ruchandanica852f42017-05-23 18:18:05 -0700384 """Mark CL <n> [n ...] with ready status <0,1>"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700385 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700386 for arg in args[:-1]:
387 helper, cl = GetGerrit(opts, arg)
388 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700389UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400390
391
Mike Frysinger15b23e42014-12-05 17:00:05 -0500392def UserActTrybotready(opts, *args):
393 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
394 num = args[-1]
395 for arg in args[:-1]:
396 helper, cl = GetGerrit(opts, arg)
397 helper.SetReview(cl, labels={'Trybot-Ready': num}, dryrun=opts.dryrun)
398UserActTrybotready.arg_min = 2
399
400
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700401def UserActSubmit(opts, *args):
402 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700403 for arg in args:
404 helper, cl = GetGerrit(opts, arg)
405 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400406
407
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700408def UserActAbandon(opts, *args):
409 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700410 for arg in args:
411 helper, cl = GetGerrit(opts, arg)
412 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400413
414
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700415def UserActRestore(opts, *args):
416 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700417 for arg in args:
418 helper, cl = GetGerrit(opts, arg)
419 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400420
421
Mike Frysinger88f27292014-06-17 09:40:45 -0700422def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700423 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500424 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700425 # Allow for optional leading '~'.
426 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
427 add_list, remove_list, invalid_list = [], [], []
428
429 for x in emails:
430 if not email_validator.match(x):
431 invalid_list.append(x)
432 elif x[0] == '~':
433 remove_list.append(x[1:])
434 else:
435 add_list.append(x)
436
437 if invalid_list:
438 cros_build_lib.Die(
439 'Invalid email address(es): %s' % ', '.join(invalid_list))
440
441 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700442 helper, cl = GetGerrit(opts, cl)
443 helper.SetReviewers(cl, add=add_list, remove=remove_list,
444 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700445
446
Allen Li38abdaa2017-03-16 13:25:02 -0700447def UserActAssign(opts, cl, assignee):
448 """Set assignee for CL <n>"""
449 helper, cl = GetGerrit(opts, cl)
450 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
451
452
Mike Frysinger88f27292014-06-17 09:40:45 -0700453def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530454 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700455 helper, cl = GetGerrit(opts, cl)
456 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530457
458
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800459def UserActTopic(opts, topic, *args):
460 """Set |topic| for CL number <n> [n ...]"""
461 for arg in args:
462 helper, arg = GetGerrit(opts, arg)
463 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
464
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700465def UserActPrivate(opts, cl, private_str):
466 """Set private bit on CL to private"""
467 try:
468 private = cros_build_lib.BooleanShellValue(private_str, False)
469 except ValueError:
470 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
471
472 helper, cl = GetGerrit(opts, cl)
473 helper.SetPrivate(cl, private)
474
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800475
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800476def UserActSethashtags(opts, cl, *args):
477 """Add/remove hashtags for CL <n> (prepend with '~' to remove)"""
478 hashtags = args
479 add = []
480 remove = []
481 for hashtag in hashtags:
482 if hashtag.startswith('~'):
483 remove.append(hashtag[1:])
484 else:
485 add.append(hashtag)
486 helper, cl = GetGerrit(opts, cl)
487 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
488
489
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700490def UserActDeletedraft(opts, *args):
Marc Herbert02448c82015-10-07 14:03:34 -0700491 """Delete draft CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700492 for arg in args:
493 helper, cl = GetGerrit(opts, arg)
494 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800495
496
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800497def UserActAccount(opts):
498 """Get user account information."""
499 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400500 acct = helper.GetAccount()
501 if opts.json:
502 json.dump(acct, sys.stdout)
503 else:
504 print('account_id:%i %s <%s>' %
505 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800506
507
Mike Frysinger108eda22018-06-06 18:45:12 -0400508def GetParser():
509 """Returns the parser to use for this module."""
510 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400511
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500512 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400513
514There is no support for doing line-by-line code review via the command line.
515This helps you manage various bits and CL status.
516
Mike Frysingera1db2c42014-06-15 00:42:48 -0700517For general Gerrit documentation, see:
518 https://gerrit-review.googlesource.com/Documentation/
519The Searching Changes page covers the search query syntax:
520 https://gerrit-review.googlesource.com/Documentation/user-search.html
521
Mike Frysinger13f23a42013-05-13 17:32:01 -0400522Example:
523 $ gerrit todo # List all the CLs that await your review.
524 $ gerrit mine # List all of your open CLs.
525 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
526 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
527 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700528Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700529 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
530ready.
531 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
532ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400533 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400534
535Actions:"""
Mike Frysinger108eda22018-06-06 18:45:12 -0400536 indent = max([len(x) - len(ACTION_PREFIX) for x in actions])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400537 for a in sorted(actions):
Mike Frysinger108eda22018-06-06 18:45:12 -0400538 cmd = a[len(ACTION_PREFIX):]
Mike Frysinger15b23e42014-12-05 17:00:05 -0500539 # Sanity check for devs adding new commands. Should be quick.
540 if cmd != cmd.lower().capitalize():
541 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
542 (cmd, cmd.lower().capitalize()))
543 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400544
Alex Klein2ab29cc2018-07-19 12:01:00 -0600545 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500546 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500547 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600548 default=site_params.EXTERNAL_GOB_INSTANCE,
549 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500550 help='Query internal Chromium Gerrit instance')
551 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600552 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500553 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600554 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500555 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400556 help='Key to sort on (number, project); use "unsorted" '
557 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700558 parser.add_argument('--raw', default=False, action='store_true',
559 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400560 parser.add_argument('--json', default=False, action='store_true',
561 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700562 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
563 dest='dryrun',
564 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500565 parser.add_argument('-v', '--verbose', default=False, action='store_true',
566 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800567 parser.add_argument('-b', '--branch',
568 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700569 parser.add_argument('--draft', default=False, action='store_true',
570 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800571 parser.add_argument('-p', '--project',
572 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800573 parser.add_argument('-t', '--topic',
574 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500575 parser.add_argument('action', help='The gerrit action to perform')
576 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400577
578 return parser
579
580
581def main(argv):
582 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500583 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400584
Mike Frysinger88f27292014-06-17 09:40:45 -0700585 # A cache of gerrit helpers we'll load on demand.
586 opts.gerrit = {}
587 opts.Freeze()
588
Mike Frysinger27e21b72018-07-12 14:20:21 -0400589 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400590 global COLOR
591 COLOR = terminal.Color(enabled=opts.color)
592
Mike Frysinger13f23a42013-05-13 17:32:01 -0400593 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400594 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400595 if functor:
596 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700597 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700598 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500599 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700600 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500601 (opts.action, arg_min))
602 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400603 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500604 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700605 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500606 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500607 except (cros_build_lib.RunCommandError, gerrit.GerritException,
608 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700609 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400610 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500611 parser.error('unknown action: %s' % (opts.action,))