blob: aa647919e2df5fe36a66d52a8525e6075f56035f [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 ',],
David James2b2e2c52014-12-02 19:32:07 -080044 'TRY': ['T ', 'Trybot Ready ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080045 'VRIF': ['V ', 'Verified ',],
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:
Mike Frysinger88f27292014-06-17 09:40:45 -070093 if cl.startswith('*'):
Alex Klein2ab29cc2018-07-19 12:01:00 -060094 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Mike Frysinger88f27292014-06-17 09:40:45 -070095 cl = cl[1:]
96 elif ':' in cl:
97 gob, cl = cl.split(':', 1)
98
99 if not gob in opts.gerrit:
100 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
101
102 return (opts.gerrit[gob], cl)
103
104
Mike Frysinger13f23a42013-05-13 17:32:01 -0400105def GetApprovalSummary(_opts, cls):
106 """Return a dict of the most important approvals"""
107 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700108 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
109 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
110 if not cats:
111 logging.warning('unknown gerrit approval type: %s', approver['type'])
112 continue
113 cat = cats[0].strip()
114 val = int(approver['value'])
115 if not cat in approvs:
116 # Ignore the extended categories in the summary view.
117 continue
118 elif approvs[cat] == '':
119 approvs[cat] = val
120 elif val < 0:
121 approvs[cat] = min(approvs[cat], val)
122 else:
123 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400124 return approvs
125
126
Mike Frysingera1b4b272017-04-05 16:11:00 -0400127def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400128 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400129 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400130 lims = {'url': 0, 'project': 0}
131
132 status = ''
133 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400134 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400135 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400136 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400137 functor = lambda x: x
138 elif approvs[cat] < 0:
139 functor = red
140 else:
141 functor = green
142 status += functor('%s:%2s ' % (cat, approvs[cat]))
143
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400144 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
145 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400146
147 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400148 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400149 functor = red if int(approver['value']) < 0 else green
150 n = functor('%2s' % approver['value'])
151 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
152 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500153 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400154
155
Mike Frysingera1b4b272017-04-05 16:11:00 -0400156def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400157 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400158 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600159 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400160 pfx = ''
161 # Special case internal Chrome GoB as that is what most devs use.
162 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600163 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
164 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400165 for cl in cls:
166 print('%s%s' % (pfx, cl['number']))
167
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400168 elif opts.json:
169 json.dump(cls, sys.stdout)
170
Mike Frysingera1b4b272017-04-05 16:11:00 -0400171 else:
172 if lims is None:
173 lims = limits(cls)
174
175 for cl in cls:
176 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
177
178
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400179def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700180 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800181 if opts.branch is not None:
182 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800183 if opts.project is not None:
184 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800185 if opts.topic is not None:
186 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800187
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400188 if helper is None:
189 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700190 return helper.Query(query, raw=raw, bypass_cache=False)
191
192
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400193def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700194 """Query gerrit and filter/clean up the results"""
195 ret = []
196
Mike Frysinger2cd56022017-01-12 20:56:27 -0500197 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400198 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400199 # Gerrit likes to return a stats record too.
200 if not 'project' in cl:
201 continue
202
203 # Strip off common leading names since the result is still
204 # unique over the whole tree.
205 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400206 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
207 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400208 if cl['project'].startswith('%s/' % pfx):
209 cl['project'] = cl['project'][len(pfx) + 1:]
210
Mike Frysinger479f1192017-09-14 22:36:30 -0400211 cl['url'] = uri_lib.ShortenUri(cl['url'])
212
Mike Frysinger13f23a42013-05-13 17:32:01 -0400213 ret.append(cl)
214
Mike Frysingerb62313a2017-06-30 16:38:58 -0400215 if opts.sort == 'unsorted':
216 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700217 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400218 key = lambda x: int(x[opts.sort])
219 else:
220 key = lambda x: x[opts.sort]
221 return sorted(ret, key=key)
222
223
Mike Frysinger13f23a42013-05-13 17:32:01 -0400224def UserActTodo(opts):
225 """List CLs needing your review"""
Mike Frysinger87690552018-12-30 22:56:06 -0500226 cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
Mike Frysingered3d7ea2017-07-10 13:14:02 -0400227 'label:Code-Review=0,user=self '
228 'NOT label:Verified<0'))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400229 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400230
231
Mike Frysingera1db2c42014-06-15 00:42:48 -0700232def UserActSearch(opts, query):
233 """List CLs matching the Gerrit <search query>"""
234 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400235 PrintCls(opts, cls)
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)
Paul Hobbs89765232015-06-24 14:07:49 -0700324
325
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700326def UserActInspect(opts, *args):
327 """Inspect CL number <n> [n ...]"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400328 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700329 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400330 helper, cl = GetGerrit(opts, arg)
331 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
332 if change:
333 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700334 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400335 logging.warning('no results found for CL %s', arg)
336 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400337
338
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700339def UserActReview(opts, *args):
340 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
341 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700342 for arg in args[:-1]:
343 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800344 helper.SetReview(cl, labels={'Code-Review': num},
345 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700346UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400347
348
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700349def UserActVerify(opts, *args):
350 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
351 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700352 for arg in args[:-1]:
353 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800354 helper.SetReview(cl, labels={'Verified': num},
355 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700356UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400357
358
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700359def UserActReady(opts, *args):
Kirtika Ruchandanica852f42017-05-23 18:18:05 -0700360 """Mark CL <n> [n ...] with ready status <0,1>"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700361 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700362 for arg in args[:-1]:
363 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800364 helper.SetReview(cl, labels={'Commit-Queue': num},
365 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700366UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400367
368
Mike Frysinger15b23e42014-12-05 17:00:05 -0500369def UserActTrybotready(opts, *args):
370 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
371 num = args[-1]
372 for arg in args[:-1]:
373 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800374 helper.SetReview(cl, labels={'Trybot-Ready': num},
375 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysinger15b23e42014-12-05 17:00:05 -0500376UserActTrybotready.arg_min = 2
377
378
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700379def UserActSubmit(opts, *args):
380 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700381 for arg in args:
382 helper, cl = GetGerrit(opts, arg)
383 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400384
385
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700386def UserActAbandon(opts, *args):
387 """Abandon CL <n> [n ...]"""
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)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400391
392
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700393def UserActRestore(opts, *args):
394 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700395 for arg in args:
396 helper, cl = GetGerrit(opts, arg)
397 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400398
399
Mike Frysinger88f27292014-06-17 09:40:45 -0700400def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700401 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500402 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700403 # Allow for optional leading '~'.
404 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
405 add_list, remove_list, invalid_list = [], [], []
406
407 for x in emails:
408 if not email_validator.match(x):
409 invalid_list.append(x)
410 elif x[0] == '~':
411 remove_list.append(x[1:])
412 else:
413 add_list.append(x)
414
415 if invalid_list:
416 cros_build_lib.Die(
417 'Invalid email address(es): %s' % ', '.join(invalid_list))
418
419 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700420 helper, cl = GetGerrit(opts, cl)
421 helper.SetReviewers(cl, add=add_list, remove=remove_list,
422 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700423
424
Allen Li38abdaa2017-03-16 13:25:02 -0700425def UserActAssign(opts, cl, assignee):
426 """Set assignee for CL <n>"""
427 helper, cl = GetGerrit(opts, cl)
428 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
429
430
Mike Frysinger88f27292014-06-17 09:40:45 -0700431def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530432 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700433 helper, cl = GetGerrit(opts, cl)
434 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530435
436
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800437def UserActTopic(opts, topic, *args):
438 """Set |topic| for CL number <n> [n ...]"""
439 for arg in args:
440 helper, arg = GetGerrit(opts, arg)
441 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
442
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700443def UserActPrivate(opts, cl, private_str):
444 """Set private bit on CL to private"""
445 try:
446 private = cros_build_lib.BooleanShellValue(private_str, False)
447 except ValueError:
448 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
449
450 helper, cl = GetGerrit(opts, cl)
451 helper.SetPrivate(cl, private)
452
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800453
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800454def UserActSethashtags(opts, cl, *args):
455 """Add/remove hashtags for CL <n> (prepend with '~' to remove)"""
456 hashtags = args
457 add = []
458 remove = []
459 for hashtag in hashtags:
460 if hashtag.startswith('~'):
461 remove.append(hashtag[1:])
462 else:
463 add.append(hashtag)
464 helper, cl = GetGerrit(opts, cl)
465 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
466
467
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700468def UserActDeletedraft(opts, *args):
Marc Herbert02448c82015-10-07 14:03:34 -0700469 """Delete draft CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700470 for arg in args:
471 helper, cl = GetGerrit(opts, arg)
472 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800473
474
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800475def UserActAccount(opts):
476 """Get user account information."""
477 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400478 acct = helper.GetAccount()
479 if opts.json:
480 json.dump(acct, sys.stdout)
481 else:
482 print('account_id:%i %s <%s>' %
483 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800484
485
Mike Frysinger108eda22018-06-06 18:45:12 -0400486def GetParser():
487 """Returns the parser to use for this module."""
488 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400489
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500490 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400491
492There is no support for doing line-by-line code review via the command line.
493This helps you manage various bits and CL status.
494
Mike Frysingera1db2c42014-06-15 00:42:48 -0700495For general Gerrit documentation, see:
496 https://gerrit-review.googlesource.com/Documentation/
497The Searching Changes page covers the search query syntax:
498 https://gerrit-review.googlesource.com/Documentation/user-search.html
499
Mike Frysinger13f23a42013-05-13 17:32:01 -0400500Example:
501 $ gerrit todo # List all the CLs that await your review.
502 $ gerrit mine # List all of your open CLs.
503 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
504 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
505 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700506Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700507 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
508ready.
509 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
510ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400511 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400512
513Actions:"""
Mike Frysinger108eda22018-06-06 18:45:12 -0400514 indent = max([len(x) - len(ACTION_PREFIX) for x in actions])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400515 for a in sorted(actions):
Mike Frysinger108eda22018-06-06 18:45:12 -0400516 cmd = a[len(ACTION_PREFIX):]
Mike Frysinger15b23e42014-12-05 17:00:05 -0500517 # Sanity check for devs adding new commands. Should be quick.
518 if cmd != cmd.lower().capitalize():
519 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
520 (cmd, cmd.lower().capitalize()))
521 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400522
Alex Klein2ab29cc2018-07-19 12:01:00 -0600523 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500524 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500525 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600526 default=site_params.EXTERNAL_GOB_INSTANCE,
527 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500528 help='Query internal Chromium Gerrit instance')
529 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600530 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500531 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600532 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500533 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400534 help='Key to sort on (number, project); use "unsorted" '
535 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700536 parser.add_argument('--raw', default=False, action='store_true',
537 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400538 parser.add_argument('--json', default=False, action='store_true',
539 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700540 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
541 dest='dryrun',
542 help='Show what would be done, but do not make changes')
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800543 parser.add_argument('--ne', '--no-emails', default=True, action='store_false',
544 dest='send_email',
545 help='Do not send email for some operations '
546 '(e.g. ready/review/trybotready/verify)')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500547 parser.add_argument('-v', '--verbose', default=False, action='store_true',
548 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800549 parser.add_argument('-b', '--branch',
550 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700551 parser.add_argument('--draft', default=False, action='store_true',
552 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800553 parser.add_argument('-p', '--project',
554 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800555 parser.add_argument('-t', '--topic',
556 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500557 parser.add_argument('action', help='The gerrit action to perform')
558 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400559
560 return parser
561
562
563def main(argv):
564 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500565 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400566
Mike Frysinger88f27292014-06-17 09:40:45 -0700567 # A cache of gerrit helpers we'll load on demand.
568 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800569
570 # Convert user friendly command line option into a gerrit parameter.
571 opts.notify = 'ALL' if opts.send_email else 'NONE'
Mike Frysinger88f27292014-06-17 09:40:45 -0700572 opts.Freeze()
573
Mike Frysinger27e21b72018-07-12 14:20:21 -0400574 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400575 global COLOR
576 COLOR = terminal.Color(enabled=opts.color)
577
Mike Frysinger13f23a42013-05-13 17:32:01 -0400578 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400579 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400580 if functor:
581 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700582 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700583 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500584 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700585 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500586 (opts.action, arg_min))
587 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400588 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500589 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700590 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500591 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500592 except (cros_build_lib.RunCommandError, gerrit.GerritException,
593 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700594 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400595 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500596 parser.error('unknown action: %s' % (opts.action,))