blob: 558e457c11e9898472ebeea42a0b6335ed9a3719 [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 Frysinger10666292018-07-12 01:03:38 -040027from chromite.lib import memoize
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
Mike Frysinger108eda22018-06-06 18:45:12 -040032# Locate actions that are exposed to the user. All functions that start
33# with "UserAct" are fair game.
34ACTION_PREFIX = 'UserAct'
35
36
Mike Frysinger031ad0b2013-05-14 18:15:34 -040037COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040038
39# Map the internal names to the ones we normally show on the web ui.
40GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080041 'COMR': ['CQ', 'Commit Queue ',],
42 'CRVW': ['CR', 'Code Review ',],
43 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080044 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040045}
46
47# Order is important -- matches the web ui. This also controls the short
48# entries that we summarize in non-verbose mode.
49GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
50
51
52def red(s):
53 return COLOR.Color(terminal.Color.RED, s)
54
55
56def green(s):
57 return COLOR.Color(terminal.Color.GREEN, s)
58
59
60def blue(s):
61 return COLOR.Color(terminal.Color.BLUE, s)
62
63
64def limits(cls):
65 """Given a dict of fields, calculate the longest string lengths
66
67 This allows you to easily format the output of many results so that the
68 various cols all line up correctly.
69 """
70 lims = {}
71 for cl in cls:
72 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040073 # Use %s rather than str() to avoid codec issues.
74 # We also do this so we can format integers.
75 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040076 return lims
77
78
Mike Frysinger88f27292014-06-17 09:40:45 -070079# TODO: This func really needs to be merged into the core gerrit logic.
80def GetGerrit(opts, cl=None):
81 """Auto pick the right gerrit instance based on the |cl|
82
83 Args:
84 opts: The general options object.
85 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
86
87 Returns:
88 A tuple of a gerrit object and a sanitized CL #.
89 """
90 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -070091 if cl is not None:
Mike Frysinger88f27292014-06-17 09:40:45 -070092 if cl.startswith('*'):
Alex Klein2ab29cc2018-07-19 12:01:00 -060093 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Mike Frysinger88f27292014-06-17 09:40:45 -070094 cl = cl[1:]
95 elif ':' in cl:
96 gob, cl = cl.split(':', 1)
97
98 if not gob in opts.gerrit:
99 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
100
101 return (opts.gerrit[gob], cl)
102
103
Mike Frysinger13f23a42013-05-13 17:32:01 -0400104def GetApprovalSummary(_opts, cls):
105 """Return a dict of the most important approvals"""
106 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700107 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
108 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
109 if not cats:
110 logging.warning('unknown gerrit approval type: %s', approver['type'])
111 continue
112 cat = cats[0].strip()
113 val = int(approver['value'])
114 if not cat in approvs:
115 # Ignore the extended categories in the summary view.
116 continue
117 elif approvs[cat] == '':
118 approvs[cat] = val
119 elif val < 0:
120 approvs[cat] = min(approvs[cat], val)
121 else:
122 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400123 return approvs
124
125
Mike Frysingera1b4b272017-04-05 16:11:00 -0400126def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400127 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400128 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400129 lims = {'url': 0, 'project': 0}
130
131 status = ''
132 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400133 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400134 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400135 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400136 functor = lambda x: x
137 elif approvs[cat] < 0:
138 functor = red
139 else:
140 functor = green
141 status += functor('%s:%2s ' % (cat, approvs[cat]))
142
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400143 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
144 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400145
146 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400147 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400148 functor = red if int(approver['value']) < 0 else green
149 n = functor('%2s' % approver['value'])
150 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
151 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500152 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400153
154
Mike Frysingera1b4b272017-04-05 16:11:00 -0400155def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400156 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400157 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600158 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400159 pfx = ''
160 # Special case internal Chrome GoB as that is what most devs use.
161 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600162 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
163 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400164 for cl in cls:
165 print('%s%s' % (pfx, cl['number']))
166
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400167 elif opts.json:
168 json.dump(cls, sys.stdout)
169
Mike Frysingera1b4b272017-04-05 16:11:00 -0400170 else:
171 if lims is None:
172 lims = limits(cls)
173
174 for cl in cls:
175 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
176
177
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400178def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700179 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800180 if opts.branch is not None:
181 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800182 if opts.project is not None:
183 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800184 if opts.topic is not None:
185 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800186
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400187 if helper is None:
188 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700189 return helper.Query(query, raw=raw, bypass_cache=False)
190
191
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400192def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700193 """Query gerrit and filter/clean up the results"""
194 ret = []
195
Mike Frysinger2cd56022017-01-12 20:56:27 -0500196 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400197 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400198 # Gerrit likes to return a stats record too.
199 if not 'project' in cl:
200 continue
201
202 # Strip off common leading names since the result is still
203 # unique over the whole tree.
204 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400205 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
206 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400207 if cl['project'].startswith('%s/' % pfx):
208 cl['project'] = cl['project'][len(pfx) + 1:]
209
Mike Frysinger479f1192017-09-14 22:36:30 -0400210 cl['url'] = uri_lib.ShortenUri(cl['url'])
211
Mike Frysinger13f23a42013-05-13 17:32:01 -0400212 ret.append(cl)
213
Mike Frysingerb62313a2017-06-30 16:38:58 -0400214 if opts.sort == 'unsorted':
215 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700216 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400217 key = lambda x: int(x[opts.sort])
218 else:
219 key = lambda x: x[opts.sort]
220 return sorted(ret, key=key)
221
222
Mike Frysinger13f23a42013-05-13 17:32:01 -0400223def UserActTodo(opts):
224 """List CLs needing your review"""
Mike Frysinger87690552018-12-30 22:56:06 -0500225 cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
Mike Frysingered3d7ea2017-07-10 13:14:02 -0400226 'label:Code-Review=0,user=self '
227 'NOT label:Verified<0'))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400228 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400229
230
Mike Frysingera1db2c42014-06-15 00:42:48 -0700231def UserActSearch(opts, query):
Harry Cutts26076b32019-02-26 15:01:29 -0800232 """List CLs matching the search query"""
Mike Frysingera1db2c42014-06-15 00:42:48 -0700233 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400234 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800235UserActSearch.usage = '<query>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400236
237
Mike Frysingera1db2c42014-06-15 00:42:48 -0700238def UserActMine(opts):
239 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700240 if opts.draft:
241 rule = 'is:draft'
242 else:
243 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500244 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700245
246
Paul Hobbs89765232015-06-24 14:07:49 -0700247def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
248 """Runs breadth first search starting from the nodes in |to_visit|
249
250 Args:
251 to_visit: the starting nodes
252 children: a function which takes a node and returns the nodes adjacent to it
253 visited_key: a function for deduplicating node visits. Defaults to the
254 identity function (lambda x: x)
255
256 Returns:
257 A list of nodes which are reachable from any node in |to_visit| by calling
258 |children| any number of times.
259 """
260 to_visit = list(to_visit)
261 seen = set(map(visited_key, to_visit))
262 for node in to_visit:
263 for child in children(node):
264 key = visited_key(child)
265 if key not in seen:
266 seen.add(key)
267 to_visit.append(child)
268 return to_visit
269
270
271def UserActDeps(opts, query):
272 """List CLs matching a query, and all transitive dependencies of those CLs"""
273 cls = _Query(opts, query, raw=False)
274
Mike Frysinger10666292018-07-12 01:03:38 -0400275 @memoize.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400276 def _QueryChange(cl, helper=None):
277 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700278
Mike Frysinger5726da92017-09-20 22:14:25 -0400279 def _ProcessDeps(cl, deps, required):
280 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700281 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400282 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400283 if not dep.remote in opts.gerrit:
284 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
285 remote=dep.remote, print_cmd=opts.debug)
286 helper = opts.gerrit[dep.remote]
287
Paul Hobbs89765232015-06-24 14:07:49 -0700288 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400289 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
290
291 # Handle empty results. If we found a commit that was pushed directly
292 # (e.g. a bot commit), then gerrit won't know about it.
293 if not changes:
294 if required:
295 logging.error('CL %s depends on %s which cannot be found',
296 cl, dep.ToGerritQueryText())
297 continue
298
299 # Our query might have matched more than one result. This can come up
300 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
301 # across multiple repos/branches. We blindly check all of them in the
302 # hopes that all open ones are what the user wants, but then again the
303 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
304 if len(changes) > 1:
305 logging.warning('CL %s has an ambiguous CQ dependency %s',
306 cl, dep.ToGerritQueryText())
307 for change in changes:
308 if change.status == 'NEW':
309 yield change
310
311 def _Children(cl):
312 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
313 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
314 yield change
315 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
316 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700317
318 transitives = _BreadthFirstSearch(
319 cls, _Children,
320 visited_key=lambda cl: cl.gerrit_number)
321
322 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400323 PrintCls(opts, transitives_raw)
Harry Cutts26076b32019-02-26 15:01:29 -0800324UserActDeps.usage = '<query>'
Paul Hobbs89765232015-06-24 14:07:49 -0700325
326
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700327def UserActInspect(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800328 """Show the details of one or more CLs"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400329 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700330 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400331 helper, cl = GetGerrit(opts, arg)
332 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
333 if change:
334 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700335 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400336 logging.warning('no results found for CL %s', arg)
337 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800338UserActInspect.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400339
340
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700341def UserActReview(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800342 """Mark CLs with a code review status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700343 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700344 for arg in args[:-1]:
345 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800346 helper.SetReview(cl, labels={'Code-Review': num},
347 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700348UserActReview.arg_min = 2
Harry Cutts26076b32019-02-26 15:01:29 -0800349UserActReview.usage = '<CLs...> <-2|-1|0|1|2>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400350
351
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700352def UserActVerify(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800353 """Mark CLs with a verified status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700354 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700355 for arg in args[:-1]:
356 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800357 helper.SetReview(cl, labels={'Verified': num},
358 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700359UserActVerify.arg_min = 2
Harry Cutts26076b32019-02-26 15:01:29 -0800360UserActVerify.usage = '<CLs...> <-1|0|1>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400361
362
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700363def UserActReady(opts, *args):
Jason D. Clinton88aed612019-04-07 20:24:05 -0600364 """Mark CLs with CQ dryrun (1) or ready (2) status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700365 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700366 for arg in args[:-1]:
367 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800368 helper.SetReview(cl, labels={'Commit-Queue': num},
369 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700370UserActReady.arg_min = 2
Jason D. Clinton88aed612019-04-07 20:24:05 -0600371UserActReady.usage = '<CLs...> <0|1|2>'
Mike Frysinger15b23e42014-12-05 17:00:05 -0500372
373
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700374def UserActSubmit(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800375 """Submit CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700376 for arg in args:
377 helper, cl = GetGerrit(opts, arg)
378 helper.SubmitChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800379UserActSubmit.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400380
381
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700382def UserActAbandon(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800383 """Abandon CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700384 for arg in args:
385 helper, cl = GetGerrit(opts, arg)
386 helper.AbandonChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800387UserActAbandon.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400388
389
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700390def UserActRestore(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800391 """Restore CLs that were abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700392 for arg in args:
393 helper, cl = GetGerrit(opts, arg)
394 helper.RestoreChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800395UserActRestore.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400396
397
Mike Frysinger88f27292014-06-17 09:40:45 -0700398def UserActReviewers(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800399 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500400 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700401 # Allow for optional leading '~'.
402 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
403 add_list, remove_list, invalid_list = [], [], []
404
405 for x in emails:
406 if not email_validator.match(x):
407 invalid_list.append(x)
408 elif x[0] == '~':
409 remove_list.append(x[1:])
410 else:
411 add_list.append(x)
412
413 if invalid_list:
414 cros_build_lib.Die(
415 'Invalid email address(es): %s' % ', '.join(invalid_list))
416
417 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700418 helper, cl = GetGerrit(opts, cl)
419 helper.SetReviewers(cl, add=add_list, remove=remove_list,
420 dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800421UserActReviewers.usage = '<CL> <emails...>'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700422
423
Allen Li38abdaa2017-03-16 13:25:02 -0700424def UserActAssign(opts, cl, assignee):
Harry Cutts26076b32019-02-26 15:01:29 -0800425 """Set the assignee for a CL"""
Allen Li38abdaa2017-03-16 13:25:02 -0700426 helper, cl = GetGerrit(opts, cl)
427 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800428UserActAssign.usage = '<CL> <assignee>'
Allen Li38abdaa2017-03-16 13:25:02 -0700429
430
Mike Frysinger88f27292014-06-17 09:40:45 -0700431def UserActMessage(opts, cl, message):
Harry Cutts26076b32019-02-26 15:01:29 -0800432 """Add a message to a CL"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700433 helper, cl = GetGerrit(opts, cl)
434 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800435UserActMessage.usage = '<CL> <message>'
Doug Anderson8119df02013-07-20 21:00:24 +0530436
437
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800438def UserActTopic(opts, topic, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800439 """Set a topic for one or more CLs"""
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800440 for arg in args:
441 helper, arg = GetGerrit(opts, arg)
442 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800443UserActTopic.usage = '<topic> <CLs...>'
444
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800445
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700446def UserActPrivate(opts, cl, private_str):
Harry Cutts26076b32019-02-26 15:01:29 -0800447 """Set the private bit on a CL to private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700448 try:
449 private = cros_build_lib.BooleanShellValue(private_str, False)
450 except ValueError:
451 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
452
453 helper, cl = GetGerrit(opts, cl)
454 helper.SetPrivate(cl, private)
Harry Cutts26076b32019-02-26 15:01:29 -0800455UserActPrivate.usage = '<CL> <private str>'
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700456
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800457
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800458def UserActSethashtags(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800459 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800460 hashtags = args
461 add = []
462 remove = []
463 for hashtag in hashtags:
464 if hashtag.startswith('~'):
465 remove.append(hashtag[1:])
466 else:
467 add.append(hashtag)
468 helper, cl = GetGerrit(opts, cl)
469 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800470UserActSethashtags.usage = '<CL> <hashtags...>'
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800471
472
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700473def UserActDeletedraft(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800474 """Delete draft CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700475 for arg in args:
476 helper, cl = GetGerrit(opts, arg)
477 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800478UserActDeletedraft.usage = '<CLs...>'
Jon Salza427fb02014-03-07 18:13:17 +0800479
480
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800481def UserActAccount(opts):
Harry Cutts26076b32019-02-26 15:01:29 -0800482 """Get the current user account information"""
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800483 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400484 acct = helper.GetAccount()
485 if opts.json:
486 json.dump(acct, sys.stdout)
487 else:
488 print('account_id:%i %s <%s>' %
489 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800490
491
Harry Cutts26076b32019-02-26 15:01:29 -0800492def _GetActionUsages():
493 """Formats a one-line usage and doc message for each action."""
494 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
495 actions.sort()
496
497 cmds = [x[len(ACTION_PREFIX):] for x in actions]
498
499 # Sanity check names for devs adding new commands. Should be quick.
500 for cmd in cmds:
501 expected_name = cmd.lower().capitalize()
502 if cmd != expected_name:
503 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
504 (cmd, expected_name))
505
506 functions = [globals()[x] for x in actions]
507 usages = [getattr(x, 'usage', '') for x in functions]
508 docs = [x.__doc__ for x in functions]
509
510 action_usages = []
511 cmd_indent = len(max(cmds, key=len))
512 usage_indent = len(max(usages, key=len))
513 for cmd, usage, doc in zip(cmds, usages, docs):
514 action_usages.append(' %-*s %-*s : %s' %
515 (cmd_indent, cmd.lower(), usage_indent, usage, doc))
516
517 return '\n'.join(action_usages)
518
519
Mike Frysinger108eda22018-06-06 18:45:12 -0400520def GetParser():
521 """Returns the parser to use for this module."""
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500522 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400523
524There is no support for doing line-by-line code review via the command line.
525This helps you manage various bits and CL status.
526
Mike Frysingera1db2c42014-06-15 00:42:48 -0700527For general Gerrit documentation, see:
528 https://gerrit-review.googlesource.com/Documentation/
529The Searching Changes page covers the search query syntax:
530 https://gerrit-review.googlesource.com/Documentation/user-search.html
531
Mike Frysinger13f23a42013-05-13 17:32:01 -0400532Example:
533 $ gerrit todo # List all the CLs that await your review.
534 $ gerrit mine # List all of your open CLs.
535 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
536 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
537 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700538Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700539 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
540ready.
541 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
542ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400543 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400544
Harry Cutts26076b32019-02-26 15:01:29 -0800545Actions:
546"""
547 usage += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400548
Alex Klein2ab29cc2018-07-19 12:01:00 -0600549 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500550 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500551 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600552 default=site_params.EXTERNAL_GOB_INSTANCE,
553 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500554 help='Query internal Chromium Gerrit instance')
555 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600556 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500557 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600558 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500559 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400560 help='Key to sort on (number, project); use "unsorted" '
561 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700562 parser.add_argument('--raw', default=False, action='store_true',
563 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400564 parser.add_argument('--json', default=False, action='store_true',
565 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700566 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
567 dest='dryrun',
568 help='Show what would be done, but do not make changes')
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800569 parser.add_argument('--ne', '--no-emails', default=True, action='store_false',
570 dest='send_email',
571 help='Do not send email for some operations '
572 '(e.g. ready/review/trybotready/verify)')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500573 parser.add_argument('-v', '--verbose', default=False, action='store_true',
574 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800575 parser.add_argument('-b', '--branch',
576 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700577 parser.add_argument('--draft', default=False, action='store_true',
578 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800579 parser.add_argument('-p', '--project',
580 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800581 parser.add_argument('-t', '--topic',
582 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500583 parser.add_argument('action', help='The gerrit action to perform')
584 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400585
586 return parser
587
588
589def main(argv):
590 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500591 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400592
Mike Frysinger88f27292014-06-17 09:40:45 -0700593 # A cache of gerrit helpers we'll load on demand.
594 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800595
596 # Convert user friendly command line option into a gerrit parameter.
597 opts.notify = 'ALL' if opts.send_email else 'NONE'
Mike Frysinger88f27292014-06-17 09:40:45 -0700598 opts.Freeze()
599
Mike Frysinger27e21b72018-07-12 14:20:21 -0400600 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400601 global COLOR
602 COLOR = terminal.Color(enabled=opts.color)
603
Mike Frysinger13f23a42013-05-13 17:32:01 -0400604 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400605 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400606 if functor:
607 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700608 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700609 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500610 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700611 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500612 (opts.action, arg_min))
613 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400614 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500615 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700616 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500617 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500618 except (cros_build_lib.RunCommandError, gerrit.GerritException,
619 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700620 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400621 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500622 parser.error('unknown action: %s' % (opts.action,))