blob: 919de2675f422a9ec661089e7b75ea542bfbfa84 [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 ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060045 'LCQ': ['L ', 'Legacy ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040046}
47
48# Order is important -- matches the web ui. This also controls the short
49# entries that we summarize in non-verbose mode.
50GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
51
52
53def red(s):
54 return COLOR.Color(terminal.Color.RED, s)
55
56
57def green(s):
58 return COLOR.Color(terminal.Color.GREEN, s)
59
60
61def blue(s):
62 return COLOR.Color(terminal.Color.BLUE, s)
63
64
65def limits(cls):
66 """Given a dict of fields, calculate the longest string lengths
67
68 This allows you to easily format the output of many results so that the
69 various cols all line up correctly.
70 """
71 lims = {}
72 for cl in cls:
73 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040074 # Use %s rather than str() to avoid codec issues.
75 # We also do this so we can format integers.
76 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040077 return lims
78
79
Mike Frysinger88f27292014-06-17 09:40:45 -070080# TODO: This func really needs to be merged into the core gerrit logic.
81def GetGerrit(opts, cl=None):
82 """Auto pick the right gerrit instance based on the |cl|
83
84 Args:
85 opts: The general options object.
86 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
87
88 Returns:
89 A tuple of a gerrit object and a sanitized CL #.
90 """
91 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -070092 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -060093 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -060094 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -060095 if cl.startswith('*'):
96 cl = cl[1:]
97 else:
98 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -070099 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])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700111 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
112 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
113 if not cats:
114 logging.warning('unknown gerrit approval type: %s', approver['type'])
115 continue
116 cat = cats[0].strip()
117 val = int(approver['value'])
118 if not cat in approvs:
119 # Ignore the extended categories in the summary view.
120 continue
121 elif approvs[cat] == '':
122 approvs[cat] = val
123 elif val < 0:
124 approvs[cat] = min(approvs[cat], val)
125 else:
126 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400127 return approvs
128
129
Mike Frysingera1b4b272017-04-05 16:11:00 -0400130def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400131 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400132 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400133 lims = {'url': 0, 'project': 0}
134
135 status = ''
136 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400137 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400138 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400139 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400140 functor = lambda x: x
141 elif approvs[cat] < 0:
142 functor = red
143 else:
144 functor = green
145 status += functor('%s:%2s ' % (cat, approvs[cat]))
146
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400147 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
148 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400149
150 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400151 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400152 functor = red if int(approver['value']) < 0 else green
153 n = functor('%2s' % approver['value'])
154 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
155 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500156 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400157
158
Mike Frysingera1b4b272017-04-05 16:11:00 -0400159def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400160 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400161 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600162 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400163 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.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600166 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
167 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400168 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 Frysinger5f938ca2017-07-19 18:29:02 -0400182def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700183 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800184 if opts.branch is not None:
185 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800186 if opts.project is not None:
187 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800188 if opts.topic is not None:
189 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800190
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400191 if helper is None:
192 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700193 return helper.Query(query, raw=raw, bypass_cache=False)
194
195
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400196def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700197 """Query gerrit and filter/clean up the results"""
198 ret = []
199
Mike Frysinger2cd56022017-01-12 20:56:27 -0500200 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400201 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400202 # Gerrit likes to return a stats record too.
203 if not 'project' in cl:
204 continue
205
206 # Strip off common leading names since the result is still
207 # unique over the whole tree.
208 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400209 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
210 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400211 if cl['project'].startswith('%s/' % pfx):
212 cl['project'] = cl['project'][len(pfx) + 1:]
213
Mike Frysinger479f1192017-09-14 22:36:30 -0400214 cl['url'] = uri_lib.ShortenUri(cl['url'])
215
Mike Frysinger13f23a42013-05-13 17:32:01 -0400216 ret.append(cl)
217
Mike Frysingerb62313a2017-06-30 16:38:58 -0400218 if opts.sort == 'unsorted':
219 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700220 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400221 key = lambda x: int(x[opts.sort])
222 else:
223 key = lambda x: x[opts.sort]
224 return sorted(ret, key=key)
225
226
Mike Frysinger13f23a42013-05-13 17:32:01 -0400227def UserActTodo(opts):
228 """List CLs needing your review"""
Mike Frysinger87690552018-12-30 22:56:06 -0500229 cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
Mike Frysingered3d7ea2017-07-10 13:14:02 -0400230 'label:Code-Review=0,user=self '
231 'NOT label:Verified<0'))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400232 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400233
234
Mike Frysingera1db2c42014-06-15 00:42:48 -0700235def UserActSearch(opts, query):
Harry Cutts26076b32019-02-26 15:01:29 -0800236 """List CLs matching the search query"""
Mike Frysingera1db2c42014-06-15 00:42:48 -0700237 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400238 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800239UserActSearch.usage = '<query>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400240
241
Mike Frysingera1db2c42014-06-15 00:42:48 -0700242def UserActMine(opts):
243 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700244 if opts.draft:
245 rule = 'is:draft'
246 else:
247 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500248 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700249
250
Paul Hobbs89765232015-06-24 14:07:49 -0700251def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
252 """Runs breadth first search starting from the nodes in |to_visit|
253
254 Args:
255 to_visit: the starting nodes
256 children: a function which takes a node and returns the nodes adjacent to it
257 visited_key: a function for deduplicating node visits. Defaults to the
258 identity function (lambda x: x)
259
260 Returns:
261 A list of nodes which are reachable from any node in |to_visit| by calling
262 |children| any number of times.
263 """
264 to_visit = list(to_visit)
265 seen = set(map(visited_key, to_visit))
266 for node in to_visit:
267 for child in children(node):
268 key = visited_key(child)
269 if key not in seen:
270 seen.add(key)
271 to_visit.append(child)
272 return to_visit
273
274
275def UserActDeps(opts, query):
276 """List CLs matching a query, and all transitive dependencies of those CLs"""
277 cls = _Query(opts, query, raw=False)
278
Mike Frysinger10666292018-07-12 01:03:38 -0400279 @memoize.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400280 def _QueryChange(cl, helper=None):
281 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700282
Mike Frysinger5726da92017-09-20 22:14:25 -0400283 def _ProcessDeps(cl, deps, required):
284 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700285 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400286 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400287 if not dep.remote in opts.gerrit:
288 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
289 remote=dep.remote, print_cmd=opts.debug)
290 helper = opts.gerrit[dep.remote]
291
Paul Hobbs89765232015-06-24 14:07:49 -0700292 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400293 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
294
295 # Handle empty results. If we found a commit that was pushed directly
296 # (e.g. a bot commit), then gerrit won't know about it.
297 if not changes:
298 if required:
299 logging.error('CL %s depends on %s which cannot be found',
300 cl, dep.ToGerritQueryText())
301 continue
302
303 # Our query might have matched more than one result. This can come up
304 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
305 # across multiple repos/branches. We blindly check all of them in the
306 # hopes that all open ones are what the user wants, but then again the
307 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
308 if len(changes) > 1:
309 logging.warning('CL %s has an ambiguous CQ dependency %s',
310 cl, dep.ToGerritQueryText())
311 for change in changes:
312 if change.status == 'NEW':
313 yield change
314
315 def _Children(cl):
316 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
317 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
318 yield change
319 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
320 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700321
322 transitives = _BreadthFirstSearch(
323 cls, _Children,
324 visited_key=lambda cl: cl.gerrit_number)
325
326 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400327 PrintCls(opts, transitives_raw)
Harry Cutts26076b32019-02-26 15:01:29 -0800328UserActDeps.usage = '<query>'
Paul Hobbs89765232015-06-24 14:07:49 -0700329
330
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700331def UserActInspect(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800332 """Show the details of one or more CLs"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400333 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700334 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400335 helper, cl = GetGerrit(opts, arg)
336 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
337 if change:
338 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700339 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400340 logging.warning('no results found for CL %s', arg)
341 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800342UserActInspect.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400343
344
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700345def UserActReview(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800346 """Mark CLs with a code review status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700347 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700348 for arg in args[:-1]:
349 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800350 helper.SetReview(cl, labels={'Code-Review': num},
351 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700352UserActReview.arg_min = 2
Harry Cutts26076b32019-02-26 15:01:29 -0800353UserActReview.usage = '<CLs...> <-2|-1|0|1|2>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400354
355
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700356def UserActVerify(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800357 """Mark CLs with a verified status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700358 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700359 for arg in args[:-1]:
360 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800361 helper.SetReview(cl, labels={'Verified': num},
362 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700363UserActVerify.arg_min = 2
Harry Cutts26076b32019-02-26 15:01:29 -0800364UserActVerify.usage = '<CLs...> <-1|0|1>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400365
366
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700367def UserActReady(opts, *args):
Jason D. Clinton88aed612019-04-07 20:24:05 -0600368 """Mark CLs with CQ dryrun (1) or ready (2) status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700369 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700370 for arg in args[:-1]:
371 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800372 helper.SetReview(cl, labels={'Commit-Queue': num},
373 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700374UserActReady.arg_min = 2
Jason D. Clinton88aed612019-04-07 20:24:05 -0600375UserActReady.usage = '<CLs...> <0|1|2>'
Mike Frysinger15b23e42014-12-05 17:00:05 -0500376
377
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700378def UserActSubmit(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800379 """Submit CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700380 for arg in args:
381 helper, cl = GetGerrit(opts, arg)
382 helper.SubmitChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800383UserActSubmit.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400384
385
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700386def UserActAbandon(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800387 """Abandon CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700388 for arg in args:
389 helper, cl = GetGerrit(opts, arg)
390 helper.AbandonChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800391UserActAbandon.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400392
393
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700394def UserActRestore(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800395 """Restore CLs that were abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700396 for arg in args:
397 helper, cl = GetGerrit(opts, arg)
398 helper.RestoreChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800399UserActRestore.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400400
401
Mike Frysinger88f27292014-06-17 09:40:45 -0700402def UserActReviewers(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800403 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500404 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700405 # Allow for optional leading '~'.
406 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
407 add_list, remove_list, invalid_list = [], [], []
408
409 for x in emails:
410 if not email_validator.match(x):
411 invalid_list.append(x)
412 elif x[0] == '~':
413 remove_list.append(x[1:])
414 else:
415 add_list.append(x)
416
417 if invalid_list:
418 cros_build_lib.Die(
419 'Invalid email address(es): %s' % ', '.join(invalid_list))
420
421 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700422 helper, cl = GetGerrit(opts, cl)
423 helper.SetReviewers(cl, add=add_list, remove=remove_list,
424 dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800425UserActReviewers.usage = '<CL> <emails...>'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700426
427
Allen Li38abdaa2017-03-16 13:25:02 -0700428def UserActAssign(opts, cl, assignee):
Harry Cutts26076b32019-02-26 15:01:29 -0800429 """Set the assignee for a CL"""
Allen Li38abdaa2017-03-16 13:25:02 -0700430 helper, cl = GetGerrit(opts, cl)
431 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800432UserActAssign.usage = '<CL> <assignee>'
Allen Li38abdaa2017-03-16 13:25:02 -0700433
434
Mike Frysinger88f27292014-06-17 09:40:45 -0700435def UserActMessage(opts, cl, message):
Harry Cutts26076b32019-02-26 15:01:29 -0800436 """Add a message to a CL"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700437 helper, cl = GetGerrit(opts, cl)
438 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800439UserActMessage.usage = '<CL> <message>'
Doug Anderson8119df02013-07-20 21:00:24 +0530440
441
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800442def UserActTopic(opts, topic, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800443 """Set a topic for one or more CLs"""
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800444 for arg in args:
445 helper, arg = GetGerrit(opts, arg)
446 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800447UserActTopic.usage = '<topic> <CLs...>'
448
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800449
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700450def UserActPrivate(opts, cl, private_str):
Harry Cutts26076b32019-02-26 15:01:29 -0800451 """Set the private bit on a CL to private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700452 try:
453 private = cros_build_lib.BooleanShellValue(private_str, False)
454 except ValueError:
455 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
456
457 helper, cl = GetGerrit(opts, cl)
458 helper.SetPrivate(cl, private)
Harry Cutts26076b32019-02-26 15:01:29 -0800459UserActPrivate.usage = '<CL> <private str>'
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700460
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800461
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800462def UserActSethashtags(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800463 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800464 hashtags = args
465 add = []
466 remove = []
467 for hashtag in hashtags:
468 if hashtag.startswith('~'):
469 remove.append(hashtag[1:])
470 else:
471 add.append(hashtag)
472 helper, cl = GetGerrit(opts, cl)
473 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800474UserActSethashtags.usage = '<CL> <hashtags...>'
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800475
476
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700477def UserActDeletedraft(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800478 """Delete draft CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700479 for arg in args:
480 helper, cl = GetGerrit(opts, arg)
481 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800482UserActDeletedraft.usage = '<CLs...>'
Jon Salza427fb02014-03-07 18:13:17 +0800483
484
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800485def UserActAccount(opts):
Harry Cutts26076b32019-02-26 15:01:29 -0800486 """Get the current user account information"""
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800487 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400488 acct = helper.GetAccount()
489 if opts.json:
490 json.dump(acct, sys.stdout)
491 else:
492 print('account_id:%i %s <%s>' %
493 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800494
495
Harry Cutts26076b32019-02-26 15:01:29 -0800496def _GetActionUsages():
497 """Formats a one-line usage and doc message for each action."""
498 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
499 actions.sort()
500
501 cmds = [x[len(ACTION_PREFIX):] for x in actions]
502
503 # Sanity check names for devs adding new commands. Should be quick.
504 for cmd in cmds:
505 expected_name = cmd.lower().capitalize()
506 if cmd != expected_name:
507 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
508 (cmd, expected_name))
509
510 functions = [globals()[x] for x in actions]
511 usages = [getattr(x, 'usage', '') for x in functions]
512 docs = [x.__doc__ for x in functions]
513
514 action_usages = []
515 cmd_indent = len(max(cmds, key=len))
516 usage_indent = len(max(usages, key=len))
517 for cmd, usage, doc in zip(cmds, usages, docs):
518 action_usages.append(' %-*s %-*s : %s' %
519 (cmd_indent, cmd.lower(), usage_indent, usage, doc))
520
521 return '\n'.join(action_usages)
522
523
Mike Frysinger108eda22018-06-06 18:45:12 -0400524def GetParser():
525 """Returns the parser to use for this module."""
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500526 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400527
528There is no support for doing line-by-line code review via the command line.
529This helps you manage various bits and CL status.
530
Mike Frysingera1db2c42014-06-15 00:42:48 -0700531For general Gerrit documentation, see:
532 https://gerrit-review.googlesource.com/Documentation/
533The Searching Changes page covers the search query syntax:
534 https://gerrit-review.googlesource.com/Documentation/user-search.html
535
Mike Frysinger13f23a42013-05-13 17:32:01 -0400536Example:
537 $ gerrit todo # List all the CLs that await your review.
538 $ gerrit mine # List all of your open CLs.
539 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
540 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
541 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800542 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
54328123.
544 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
545CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700546Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700547 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
548ready.
549 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
550ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400551 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400552
Harry Cutts26076b32019-02-26 15:01:29 -0800553Actions:
554"""
555 usage += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400556
Alex Klein2ab29cc2018-07-19 12:01:00 -0600557 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500558 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500559 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600560 default=site_params.EXTERNAL_GOB_INSTANCE,
561 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500562 help='Query internal Chromium Gerrit instance')
563 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600564 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500565 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600566 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500567 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400568 help='Key to sort on (number, project); use "unsorted" '
569 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700570 parser.add_argument('--raw', default=False, action='store_true',
571 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400572 parser.add_argument('--json', default=False, action='store_true',
573 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700574 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
575 dest='dryrun',
576 help='Show what would be done, but do not make changes')
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800577 parser.add_argument('--ne', '--no-emails', default=True, action='store_false',
578 dest='send_email',
579 help='Do not send email for some operations '
580 '(e.g. ready/review/trybotready/verify)')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500581 parser.add_argument('-v', '--verbose', default=False, action='store_true',
582 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800583 parser.add_argument('-b', '--branch',
584 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700585 parser.add_argument('--draft', default=False, action='store_true',
586 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800587 parser.add_argument('-p', '--project',
588 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800589 parser.add_argument('-t', '--topic',
590 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500591 parser.add_argument('action', help='The gerrit action to perform')
592 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400593
594 return parser
595
596
597def main(argv):
598 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500599 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400600
Mike Frysinger88f27292014-06-17 09:40:45 -0700601 # A cache of gerrit helpers we'll load on demand.
602 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800603
604 # Convert user friendly command line option into a gerrit parameter.
605 opts.notify = 'ALL' if opts.send_email else 'NONE'
Mike Frysinger88f27292014-06-17 09:40:45 -0700606 opts.Freeze()
607
Mike Frysinger27e21b72018-07-12 14:20:21 -0400608 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400609 global COLOR
610 COLOR = terminal.Color(enabled=opts.color)
611
Mike Frysinger13f23a42013-05-13 17:32:01 -0400612 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400613 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400614 if functor:
615 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700616 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700617 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500618 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700619 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500620 (opts.action, arg_min))
621 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400622 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500623 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700624 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500625 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500626 except (cros_build_lib.RunCommandError, gerrit.GerritException,
627 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700628 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400629 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500630 parser.error('unknown action: %s' % (opts.action,))