blob: 3d1f23bb3e06eaa239ae19bf521d2567d7d7655a [file] [log] [blame]
Mike Frysinger13f23a42013-05-13 17:32:01 -04001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Mike Frysinger08737512014-02-07 22:58:26 -05005"""A command line interface to Gerrit-on-borg instances.
Mike Frysinger13f23a42013-05-13 17:32:01 -04006
7Internal Note:
8To expose a function directly to the command line interface, name your function
9with the prefix "UserAct".
10"""
11
Mike Frysinger31ff6f92014-02-08 04:33:03 -050012from __future__ import print_function
13
Mike Frysinger13f23a42013-05-13 17:32:01 -040014import inspect
Mike Frysinger87c74ce2017-04-04 16:12:31 -040015import json
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070016import re
Mike Frysinger87c74ce2017-04-04 16:12:31 -040017import sys
Mike Frysinger13f23a42013-05-13 17:32:01 -040018
Aviv Keshetb7519e12016-10-04 00:50:00 -070019from chromite.lib import config_lib
20from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040021from chromite.lib import commandline
22from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070023from chromite.lib import cros_logging as logging
Mike Frysinger13f23a42013-05-13 17:32:01 -040024from chromite.lib import gerrit
Mathieu Olivari04b4d522014-12-18 17:26:34 -080025from chromite.lib import git
Mike Frysingerc85d8162014-02-08 00:45:21 -050026from chromite.lib import gob_util
Mike Frysinger13f23a42013-05-13 17:32:01 -040027from chromite.lib import terminal
28
29
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -070030site_config = config_lib.GetConfig()
31
32
Mike Frysinger031ad0b2013-05-14 18:15:34 -040033COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040034
35# Map the internal names to the ones we normally show on the web ui.
36GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080037 'COMR': ['CQ', 'Commit Queue ',],
38 'CRVW': ['CR', 'Code Review ',],
39 'SUBM': ['S ', 'Submitted ',],
David James2b2e2c52014-12-02 19:32:07 -080040 'TRY': ['T ', 'Trybot Ready ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080041 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040042}
43
44# Order is important -- matches the web ui. This also controls the short
45# entries that we summarize in non-verbose mode.
46GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
47
48
49def red(s):
50 return COLOR.Color(terminal.Color.RED, s)
51
52
53def green(s):
54 return COLOR.Color(terminal.Color.GREEN, s)
55
56
57def blue(s):
58 return COLOR.Color(terminal.Color.BLUE, s)
59
60
61def limits(cls):
62 """Given a dict of fields, calculate the longest string lengths
63
64 This allows you to easily format the output of many results so that the
65 various cols all line up correctly.
66 """
67 lims = {}
68 for cl in cls:
69 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040070 # Use %s rather than str() to avoid codec issues.
71 # We also do this so we can format integers.
72 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040073 return lims
74
75
Mike Frysinger88f27292014-06-17 09:40:45 -070076# TODO: This func really needs to be merged into the core gerrit logic.
77def GetGerrit(opts, cl=None):
78 """Auto pick the right gerrit instance based on the |cl|
79
80 Args:
81 opts: The general options object.
82 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
83
84 Returns:
85 A tuple of a gerrit object and a sanitized CL #.
86 """
87 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -070088 if cl is not None:
Mike Frysinger88f27292014-06-17 09:40:45 -070089 if cl.startswith('*'):
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -070090 gob = site_config.params.INTERNAL_GOB_INSTANCE
Mike Frysinger88f27292014-06-17 09:40:45 -070091 cl = cl[1:]
92 elif ':' in cl:
93 gob, cl = cl.split(':', 1)
94
95 if not gob in opts.gerrit:
96 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
97
98 return (opts.gerrit[gob], cl)
99
100
Mike Frysinger13f23a42013-05-13 17:32:01 -0400101def GetApprovalSummary(_opts, cls):
102 """Return a dict of the most important approvals"""
103 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
104 if 'approvals' in cls['currentPatchSet']:
105 for approver in cls['currentPatchSet']['approvals']:
106 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
107 if not cats:
Ralph Nathan446aee92015-03-23 14:44:56 -0700108 logging.warning('unknown gerrit approval type: %s', approver['type'])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400109 continue
110 cat = cats[0].strip()
111 val = int(approver['value'])
112 if not cat in approvs:
113 # Ignore the extended categories in the summary view.
114 continue
Mike Frysingera0313d02017-07-10 16:44:43 -0400115 elif approvs[cat] == '':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400116 approvs[cat] = val
117 elif val < 0:
118 approvs[cat] = min(approvs[cat], val)
119 else:
120 approvs[cat] = max(approvs[cat], val)
121 return approvs
122
123
Mike Frysingera1b4b272017-04-05 16:11:00 -0400124def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400125 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400126 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400127 lims = {'url': 0, 'project': 0}
128
129 status = ''
130 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400131 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400132 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400133 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400134 functor = lambda x: x
135 elif approvs[cat] < 0:
136 functor = red
137 else:
138 functor = green
139 status += functor('%s:%2s ' % (cat, approvs[cat]))
140
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400141 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
142 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400143
144 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400145 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400146 functor = red if int(approver['value']) < 0 else green
147 n = functor('%2s' % approver['value'])
148 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
149 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500150 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400151
152
Mike Frysingera1b4b272017-04-05 16:11:00 -0400153def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400154 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400155 if opts.raw:
156 pfx = ''
157 # Special case internal Chrome GoB as that is what most devs use.
158 # They can always redirect the list elsewhere via the -g option.
159 if opts.gob == site_config.params.INTERNAL_GOB_INSTANCE:
160 pfx = site_config.params.INTERNAL_CHANGE_PREFIX
161 for cl in cls:
162 print('%s%s' % (pfx, cl['number']))
163
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400164 elif opts.json:
165 json.dump(cls, sys.stdout)
166
Mike Frysingera1b4b272017-04-05 16:11:00 -0400167 else:
168 if lims is None:
169 lims = limits(cls)
170
171 for cl in cls:
172 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
173
174
Mike Frysinger13f23a42013-05-13 17:32:01 -0400175def _MyUserInfo():
Mike Frysinger2cd56022017-01-12 20:56:27 -0500176 """Try to return e-mail addresses used by the active user."""
177 return [git.GetProjectUserEmail(constants.CHROMITE_DIR)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400178
179
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400180def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700181 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800182 if opts.branch is not None:
183 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800184 if opts.project is not None:
185 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800186 if opts.topic is not None:
187 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800188
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400189 if helper is None:
190 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700191 return helper.Query(query, raw=raw, bypass_cache=False)
192
193
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400194def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700195 """Query gerrit and filter/clean up the results"""
196 ret = []
197
Mike Frysinger2cd56022017-01-12 20:56:27 -0500198 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400199 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400200 # Gerrit likes to return a stats record too.
201 if not 'project' in cl:
202 continue
203
204 # Strip off common leading names since the result is still
205 # unique over the whole tree.
206 if not opts.verbose:
Mike Frysingerc0fc8de2017-04-04 17:49:27 -0400207 for pfx in ('chromeos', 'chromiumos', 'external', 'overlays', 'platform',
Mike Frysingere5e78272014-06-15 00:41:30 -0700208 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400209 if cl['project'].startswith('%s/' % pfx):
210 cl['project'] = cl['project'][len(pfx) + 1:]
211
212 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 IsApprover(cl, users):
224 """See if the approvers in |cl| is listed in |users|"""
225 # See if we are listed in the approvals list. We have to parse
226 # this by hand as the gerrit query system doesn't support it :(
227 # http://code.google.com/p/gerrit/issues/detail?id=1235
228 if 'approvals' not in cl['currentPatchSet']:
229 return False
230
231 if isinstance(users, basestring):
232 users = (users,)
233
234 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700235 if (approver['by']['email'] in users and
236 approver['type'] == 'CRVW' and
237 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400238 return True
239
240 return False
241
242
243def UserActTodo(opts):
244 """List CLs needing your review"""
Mike Frysinger2cd56022017-01-12 20:56:27 -0500245 emails = _MyUserInfo()
246 cls = FilteredQuery(opts, 'reviewer:self status:open NOT owner:self')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400247 cls = [x for x in cls if not IsApprover(x, emails)]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400248 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400249
250
Mike Frysingera1db2c42014-06-15 00:42:48 -0700251def UserActSearch(opts, query):
252 """List CLs matching the Gerrit <search query>"""
253 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400254 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400255
256
Mike Frysingera1db2c42014-06-15 00:42:48 -0700257def UserActMine(opts):
258 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700259 if opts.draft:
260 rule = 'is:draft'
261 else:
262 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500263 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700264
265
Paul Hobbs89765232015-06-24 14:07:49 -0700266def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
267 """Runs breadth first search starting from the nodes in |to_visit|
268
269 Args:
270 to_visit: the starting nodes
271 children: a function which takes a node and returns the nodes adjacent to it
272 visited_key: a function for deduplicating node visits. Defaults to the
273 identity function (lambda x: x)
274
275 Returns:
276 A list of nodes which are reachable from any node in |to_visit| by calling
277 |children| any number of times.
278 """
279 to_visit = list(to_visit)
280 seen = set(map(visited_key, to_visit))
281 for node in to_visit:
282 for child in children(node):
283 key = visited_key(child)
284 if key not in seen:
285 seen.add(key)
286 to_visit.append(child)
287 return to_visit
288
289
290def UserActDeps(opts, query):
291 """List CLs matching a query, and all transitive dependencies of those CLs"""
292 cls = _Query(opts, query, raw=False)
293
294 @cros_build_lib.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400295 def _QueryChange(cl, helper=None):
296 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700297
Mike Frysinger5726da92017-09-20 22:14:25 -0400298 def _ProcessDeps(cl, deps, required):
299 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700300 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400301 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400302 if not dep.remote in opts.gerrit:
303 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
304 remote=dep.remote, print_cmd=opts.debug)
305 helper = opts.gerrit[dep.remote]
306
Paul Hobbs89765232015-06-24 14:07:49 -0700307 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400308 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
309
310 # Handle empty results. If we found a commit that was pushed directly
311 # (e.g. a bot commit), then gerrit won't know about it.
312 if not changes:
313 if required:
314 logging.error('CL %s depends on %s which cannot be found',
315 cl, dep.ToGerritQueryText())
316 continue
317
318 # Our query might have matched more than one result. This can come up
319 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
320 # across multiple repos/branches. We blindly check all of them in the
321 # hopes that all open ones are what the user wants, but then again the
322 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
323 if len(changes) > 1:
324 logging.warning('CL %s has an ambiguous CQ dependency %s',
325 cl, dep.ToGerritQueryText())
326 for change in changes:
327 if change.status == 'NEW':
328 yield change
329
330 def _Children(cl):
331 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
332 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
333 yield change
334 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
335 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700336
337 transitives = _BreadthFirstSearch(
338 cls, _Children,
339 visited_key=lambda cl: cl.gerrit_number)
340
341 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400342 PrintCls(opts, transitives_raw)
Paul Hobbs89765232015-06-24 14:07:49 -0700343
344
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700345def UserActInspect(opts, *args):
346 """Inspect CL number <n> [n ...]"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400347 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700348 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400349 helper, cl = GetGerrit(opts, arg)
350 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
351 if change:
352 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700353 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400354 logging.warning('no results found for CL %s', arg)
355 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400356
357
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700358def UserActReview(opts, *args):
359 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
360 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700361 for arg in args[:-1]:
362 helper, cl = GetGerrit(opts, arg)
363 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700364UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400365
366
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700367def UserActVerify(opts, *args):
368 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
369 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700370 for arg in args[:-1]:
371 helper, cl = GetGerrit(opts, arg)
372 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700373UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400374
375
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700376def UserActReady(opts, *args):
Kirtika Ruchandanica852f42017-05-23 18:18:05 -0700377 """Mark CL <n> [n ...] with ready status <0,1>"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700378 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700379 for arg in args[:-1]:
380 helper, cl = GetGerrit(opts, arg)
381 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700382UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400383
384
Mike Frysinger15b23e42014-12-05 17:00:05 -0500385def UserActTrybotready(opts, *args):
386 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
387 num = args[-1]
388 for arg in args[:-1]:
389 helper, cl = GetGerrit(opts, arg)
390 helper.SetReview(cl, labels={'Trybot-Ready': num}, dryrun=opts.dryrun)
391UserActTrybotready.arg_min = 2
392
393
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700394def UserActSubmit(opts, *args):
395 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700396 for arg in args:
397 helper, cl = GetGerrit(opts, arg)
398 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400399
400
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700401def UserActAbandon(opts, *args):
402 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700403 for arg in args:
404 helper, cl = GetGerrit(opts, arg)
405 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400406
407
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700408def UserActRestore(opts, *args):
409 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700410 for arg in args:
411 helper, cl = GetGerrit(opts, arg)
412 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400413
414
Mike Frysinger88f27292014-06-17 09:40:45 -0700415def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700416 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500417 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700418 # Allow for optional leading '~'.
419 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
420 add_list, remove_list, invalid_list = [], [], []
421
422 for x in emails:
423 if not email_validator.match(x):
424 invalid_list.append(x)
425 elif x[0] == '~':
426 remove_list.append(x[1:])
427 else:
428 add_list.append(x)
429
430 if invalid_list:
431 cros_build_lib.Die(
432 'Invalid email address(es): %s' % ', '.join(invalid_list))
433
434 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700435 helper, cl = GetGerrit(opts, cl)
436 helper.SetReviewers(cl, add=add_list, remove=remove_list,
437 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700438
439
Allen Li38abdaa2017-03-16 13:25:02 -0700440def UserActAssign(opts, cl, assignee):
441 """Set assignee for CL <n>"""
442 helper, cl = GetGerrit(opts, cl)
443 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
444
445
Mike Frysinger88f27292014-06-17 09:40:45 -0700446def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530447 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700448 helper, cl = GetGerrit(opts, cl)
449 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530450
451
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800452def UserActTopic(opts, topic, *args):
453 """Set |topic| for CL number <n> [n ...]"""
454 for arg in args:
455 helper, arg = GetGerrit(opts, arg)
456 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
457
458
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800459def UserActSethashtags(opts, cl, *args):
460 """Add/remove hashtags for CL <n> (prepend with '~' to remove)"""
461 hashtags = args
462 add = []
463 remove = []
464 for hashtag in hashtags:
465 if hashtag.startswith('~'):
466 remove.append(hashtag[1:])
467 else:
468 add.append(hashtag)
469 helper, cl = GetGerrit(opts, cl)
470 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
471
472
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700473def UserActDeletedraft(opts, *args):
Marc Herbert02448c82015-10-07 14:03:34 -0700474 """Delete draft CL <n> [n ...]"""
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)
Jon Salza427fb02014-03-07 18:13:17 +0800478
479
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800480def UserActAccount(opts):
481 """Get user account information."""
482 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400483 acct = helper.GetAccount()
484 if opts.json:
485 json.dump(acct, sys.stdout)
486 else:
487 print('account_id:%i %s <%s>' %
488 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800489
490
Mike Frysinger13f23a42013-05-13 17:32:01 -0400491def main(argv):
492 # Locate actions that are exposed to the user. All functions that start
493 # with "UserAct" are fair game.
494 act_pfx = 'UserAct'
495 actions = [x for x in globals() if x.startswith(act_pfx)]
496
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500497 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400498
499There is no support for doing line-by-line code review via the command line.
500This helps you manage various bits and CL status.
501
Mike Frysingera1db2c42014-06-15 00:42:48 -0700502For general Gerrit documentation, see:
503 https://gerrit-review.googlesource.com/Documentation/
504The Searching Changes page covers the search query syntax:
505 https://gerrit-review.googlesource.com/Documentation/user-search.html
506
Mike Frysinger13f23a42013-05-13 17:32:01 -0400507Example:
508 $ gerrit todo # List all the CLs that await your review.
509 $ gerrit mine # List all of your open CLs.
510 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
511 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
512 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700513Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700514 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
515ready.
516 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
517ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400518 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400519
520Actions:"""
521 indent = max([len(x) - len(act_pfx) for x in actions])
522 for a in sorted(actions):
Mike Frysinger15b23e42014-12-05 17:00:05 -0500523 cmd = a[len(act_pfx):]
524 # Sanity check for devs adding new commands. Should be quick.
525 if cmd != cmd.lower().capitalize():
526 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
527 (cmd, cmd.lower().capitalize()))
528 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400529
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500530 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500531 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700532 default=site_config.params.EXTERNAL_GOB_INSTANCE,
533 const=site_config.params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500534 help='Query internal Chromium Gerrit instance')
535 parser.add_argument('-g', '--gob',
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700536 default=site_config.params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500537 help=('Gerrit (on borg) instance to query (default: %s)' %
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700538 (site_config.params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500539 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400540 help='Key to sort on (number, project); use "unsorted" '
541 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700542 parser.add_argument('--raw', default=False, action='store_true',
543 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400544 parser.add_argument('--json', default=False, action='store_true',
545 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700546 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
547 dest='dryrun',
548 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500549 parser.add_argument('-v', '--verbose', default=False, action='store_true',
550 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800551 parser.add_argument('-b', '--branch',
552 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700553 parser.add_argument('--draft', default=False, action='store_true',
554 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800555 parser.add_argument('-p', '--project',
556 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800557 parser.add_argument('-t', '--topic',
558 help='Limit output to the specific topic')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500559 parser.add_argument('args', nargs='+')
560 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400561
Mike Frysinger88f27292014-06-17 09:40:45 -0700562 # A cache of gerrit helpers we'll load on demand.
563 opts.gerrit = {}
564 opts.Freeze()
565
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400566 # pylint: disable=W0603
567 global COLOR
568 COLOR = terminal.Color(enabled=opts.color)
569
Mike Frysinger13f23a42013-05-13 17:32:01 -0400570 # Now look up the requested user action and run it.
Mike Frysinger88f27292014-06-17 09:40:45 -0700571 cmd = opts.args[0].lower()
572 args = opts.args[1:]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400573 functor = globals().get(act_pfx + cmd.capitalize())
574 if functor:
575 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700576 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700577 arg_min = getattr(functor, 'arg_min', len(argspec.args))
578 if len(args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700579 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700580 (cmd, arg_min))
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700581 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400582 parser.error('incorrect number of args: %s expects %s' %
583 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700584 try:
585 functor(opts, *args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500586 except (cros_build_lib.RunCommandError, gerrit.GerritException,
587 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700588 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400589 else:
590 parser.error('unknown action: %s' % (cmd,))