blob: e06e9723adefda16683ee2fe41dc06e27800edbf [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Mike Frysinger13f23a42013-05-13 17:32:01 -04002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysinger08737512014-02-07 22:58:26 -05006"""A command line interface to Gerrit-on-borg instances.
Mike Frysinger13f23a42013-05-13 17:32:01 -04007
8Internal Note:
9To expose a function directly to the command line interface, name your function
10with the prefix "UserAct".
11"""
12
Mike Frysinger31ff6f92014-02-08 04:33:03 -050013from __future__ import print_function
14
Mike Frysinger13f23a42013-05-13 17:32:01 -040015import inspect
Mike Frysinger87c74ce2017-04-04 16:12:31 -040016import json
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070017import re
Mike Frysinger87c74ce2017-04-04 16:12:31 -040018import sys
Mike Frysinger13f23a42013-05-13 17:32:01 -040019
Aviv Keshetb7519e12016-10-04 00:50:00 -070020from chromite.lib import config_lib
21from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040022from chromite.lib import commandline
23from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070024from chromite.lib import cros_logging as logging
Mike Frysinger13f23a42013-05-13 17:32:01 -040025from chromite.lib import gerrit
Mathieu Olivari04b4d522014-12-18 17:26:34 -080026from chromite.lib import git
Mike Frysingerc85d8162014-02-08 00:45:21 -050027from chromite.lib import gob_util
Mike Frysinger13f23a42013-05-13 17:32:01 -040028from chromite.lib import terminal
29
30
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -070031site_config = config_lib.GetConfig()
32
33
Mike Frysinger108eda22018-06-06 18:45:12 -040034# Locate actions that are exposed to the user. All functions that start
35# with "UserAct" are fair game.
36ACTION_PREFIX = 'UserAct'
37
38
Mike Frysinger031ad0b2013-05-14 18:15:34 -040039COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040040
41# Map the internal names to the ones we normally show on the web ui.
42GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080043 'COMR': ['CQ', 'Commit Queue ',],
44 'CRVW': ['CR', 'Code Review ',],
45 'SUBM': ['S ', 'Submitted ',],
David James2b2e2c52014-12-02 19:32:07 -080046 'TRY': ['T ', 'Trybot Ready ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080047 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040048}
49
50# Order is important -- matches the web ui. This also controls the short
51# entries that we summarize in non-verbose mode.
52GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
53
54
55def red(s):
56 return COLOR.Color(terminal.Color.RED, s)
57
58
59def green(s):
60 return COLOR.Color(terminal.Color.GREEN, s)
61
62
63def blue(s):
64 return COLOR.Color(terminal.Color.BLUE, s)
65
66
67def limits(cls):
68 """Given a dict of fields, calculate the longest string lengths
69
70 This allows you to easily format the output of many results so that the
71 various cols all line up correctly.
72 """
73 lims = {}
74 for cl in cls:
75 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040076 # Use %s rather than str() to avoid codec issues.
77 # We also do this so we can format integers.
78 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040079 return lims
80
81
Mike Frysinger88f27292014-06-17 09:40:45 -070082# TODO: This func really needs to be merged into the core gerrit logic.
83def GetGerrit(opts, cl=None):
84 """Auto pick the right gerrit instance based on the |cl|
85
86 Args:
87 opts: The general options object.
88 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
89
90 Returns:
91 A tuple of a gerrit object and a sanitized CL #.
92 """
93 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -070094 if cl is not None:
Mike Frysinger88f27292014-06-17 09:40:45 -070095 if cl.startswith('*'):
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -070096 gob = site_config.params.INTERNAL_GOB_INSTANCE
Mike Frysinger88f27292014-06-17 09:40:45 -070097 cl = cl[1:]
98 elif ':' in cl:
99 gob, cl = cl.split(':', 1)
100
101 if not gob in opts.gerrit:
102 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
103
104 return (opts.gerrit[gob], cl)
105
106
Mike Frysinger13f23a42013-05-13 17:32:01 -0400107def GetApprovalSummary(_opts, cls):
108 """Return a dict of the most important approvals"""
109 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
110 if 'approvals' in cls['currentPatchSet']:
111 for approver in cls['currentPatchSet']['approvals']:
112 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
113 if not cats:
Ralph Nathan446aee92015-03-23 14:44:56 -0700114 logging.warning('unknown gerrit approval type: %s', approver['type'])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400115 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
Mike Frysingera0313d02017-07-10 16:44:43 -0400121 elif approvs[cat] == '':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400122 approvs[cat] = val
123 elif val < 0:
124 approvs[cat] = min(approvs[cat], val)
125 else:
126 approvs[cat] = max(approvs[cat], val)
127 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:
162 pfx = ''
163 # Special case internal Chrome GoB as that is what most devs use.
164 # They can always redirect the list elsewhere via the -g option.
165 if opts.gob == site_config.params.INTERNAL_GOB_INSTANCE:
166 pfx = site_config.params.INTERNAL_CHANGE_PREFIX
167 for cl in cls:
168 print('%s%s' % (pfx, cl['number']))
169
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400170 elif opts.json:
171 json.dump(cls, sys.stdout)
172
Mike Frysingera1b4b272017-04-05 16:11:00 -0400173 else:
174 if lims is None:
175 lims = limits(cls)
176
177 for cl in cls:
178 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
179
180
Mike Frysinger13f23a42013-05-13 17:32:01 -0400181def _MyUserInfo():
Mike Frysinger2cd56022017-01-12 20:56:27 -0500182 """Try to return e-mail addresses used by the active user."""
183 return [git.GetProjectUserEmail(constants.CHROMITE_DIR)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400184
185
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400186def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700187 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800188 if opts.branch is not None:
189 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800190 if opts.project is not None:
191 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800192 if opts.topic is not None:
193 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800194
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400195 if helper is None:
196 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700197 return helper.Query(query, raw=raw, bypass_cache=False)
198
199
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400200def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700201 """Query gerrit and filter/clean up the results"""
202 ret = []
203
Mike Frysinger2cd56022017-01-12 20:56:27 -0500204 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400205 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400206 # Gerrit likes to return a stats record too.
207 if not 'project' in cl:
208 continue
209
210 # Strip off common leading names since the result is still
211 # unique over the whole tree.
212 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400213 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
214 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400215 if cl['project'].startswith('%s/' % pfx):
216 cl['project'] = cl['project'][len(pfx) + 1:]
217
218 ret.append(cl)
219
Mike Frysingerb62313a2017-06-30 16:38:58 -0400220 if opts.sort == 'unsorted':
221 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700222 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400223 key = lambda x: int(x[opts.sort])
224 else:
225 key = lambda x: x[opts.sort]
226 return sorted(ret, key=key)
227
228
Mike Frysinger13f23a42013-05-13 17:32:01 -0400229def IsApprover(cl, users):
230 """See if the approvers in |cl| is listed in |users|"""
231 # See if we are listed in the approvals list. We have to parse
232 # this by hand as the gerrit query system doesn't support it :(
233 # http://code.google.com/p/gerrit/issues/detail?id=1235
234 if 'approvals' not in cl['currentPatchSet']:
235 return False
236
237 if isinstance(users, basestring):
238 users = (users,)
239
240 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700241 if (approver['by']['email'] in users and
242 approver['type'] == 'CRVW' and
243 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400244 return True
245
246 return False
247
248
249def UserActTodo(opts):
250 """List CLs needing your review"""
Mike Frysinger2cd56022017-01-12 20:56:27 -0500251 emails = _MyUserInfo()
252 cls = FilteredQuery(opts, 'reviewer:self status:open NOT owner:self')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400253 cls = [x for x in cls if not IsApprover(x, emails)]
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 UserActSearch(opts, query):
258 """List CLs matching the Gerrit <search query>"""
259 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400260 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400261
262
Mike Frysingera1db2c42014-06-15 00:42:48 -0700263def UserActMine(opts):
264 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700265 if opts.draft:
266 rule = 'is:draft'
267 else:
268 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500269 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700270
271
Paul Hobbs89765232015-06-24 14:07:49 -0700272def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
273 """Runs breadth first search starting from the nodes in |to_visit|
274
275 Args:
276 to_visit: the starting nodes
277 children: a function which takes a node and returns the nodes adjacent to it
278 visited_key: a function for deduplicating node visits. Defaults to the
279 identity function (lambda x: x)
280
281 Returns:
282 A list of nodes which are reachable from any node in |to_visit| by calling
283 |children| any number of times.
284 """
285 to_visit = list(to_visit)
286 seen = set(map(visited_key, to_visit))
287 for node in to_visit:
288 for child in children(node):
289 key = visited_key(child)
290 if key not in seen:
291 seen.add(key)
292 to_visit.append(child)
293 return to_visit
294
295
296def UserActDeps(opts, query):
297 """List CLs matching a query, and all transitive dependencies of those CLs"""
298 cls = _Query(opts, query, raw=False)
299
300 @cros_build_lib.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400301 def _QueryChange(cl, helper=None):
302 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700303
Mike Frysinger5726da92017-09-20 22:14:25 -0400304 def _ProcessDeps(cl, deps, required):
305 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700306 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400307 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400308 if not dep.remote in opts.gerrit:
309 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
310 remote=dep.remote, print_cmd=opts.debug)
311 helper = opts.gerrit[dep.remote]
312
Paul Hobbs89765232015-06-24 14:07:49 -0700313 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400314 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
315
316 # Handle empty results. If we found a commit that was pushed directly
317 # (e.g. a bot commit), then gerrit won't know about it.
318 if not changes:
319 if required:
320 logging.error('CL %s depends on %s which cannot be found',
321 cl, dep.ToGerritQueryText())
322 continue
323
324 # Our query might have matched more than one result. This can come up
325 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
326 # across multiple repos/branches. We blindly check all of them in the
327 # hopes that all open ones are what the user wants, but then again the
328 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
329 if len(changes) > 1:
330 logging.warning('CL %s has an ambiguous CQ dependency %s',
331 cl, dep.ToGerritQueryText())
332 for change in changes:
333 if change.status == 'NEW':
334 yield change
335
336 def _Children(cl):
337 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
338 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
339 yield change
340 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
341 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700342
343 transitives = _BreadthFirstSearch(
344 cls, _Children,
345 visited_key=lambda cl: cl.gerrit_number)
346
347 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400348 PrintCls(opts, transitives_raw)
Paul Hobbs89765232015-06-24 14:07:49 -0700349
350
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700351def UserActInspect(opts, *args):
352 """Inspect CL number <n> [n ...]"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400353 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700354 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400355 helper, cl = GetGerrit(opts, arg)
356 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
357 if change:
358 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700359 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400360 logging.warning('no results found for CL %s', arg)
361 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400362
363
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700364def UserActReview(opts, *args):
365 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
366 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700367 for arg in args[:-1]:
368 helper, cl = GetGerrit(opts, arg)
369 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700370UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400371
372
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700373def UserActVerify(opts, *args):
374 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
375 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700376 for arg in args[:-1]:
377 helper, cl = GetGerrit(opts, arg)
378 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700379UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400380
381
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700382def UserActReady(opts, *args):
Kirtika Ruchandanica852f42017-05-23 18:18:05 -0700383 """Mark CL <n> [n ...] with ready status <0,1>"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700384 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700385 for arg in args[:-1]:
386 helper, cl = GetGerrit(opts, arg)
387 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700388UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400389
390
Mike Frysinger15b23e42014-12-05 17:00:05 -0500391def UserActTrybotready(opts, *args):
392 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
393 num = args[-1]
394 for arg in args[:-1]:
395 helper, cl = GetGerrit(opts, arg)
396 helper.SetReview(cl, labels={'Trybot-Ready': num}, dryrun=opts.dryrun)
397UserActTrybotready.arg_min = 2
398
399
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700400def UserActSubmit(opts, *args):
401 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700402 for arg in args:
403 helper, cl = GetGerrit(opts, arg)
404 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400405
406
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700407def UserActAbandon(opts, *args):
408 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700409 for arg in args:
410 helper, cl = GetGerrit(opts, arg)
411 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400412
413
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700414def UserActRestore(opts, *args):
415 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700416 for arg in args:
417 helper, cl = GetGerrit(opts, arg)
418 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400419
420
Mike Frysinger88f27292014-06-17 09:40:45 -0700421def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700422 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500423 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700424 # Allow for optional leading '~'.
425 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
426 add_list, remove_list, invalid_list = [], [], []
427
428 for x in emails:
429 if not email_validator.match(x):
430 invalid_list.append(x)
431 elif x[0] == '~':
432 remove_list.append(x[1:])
433 else:
434 add_list.append(x)
435
436 if invalid_list:
437 cros_build_lib.Die(
438 'Invalid email address(es): %s' % ', '.join(invalid_list))
439
440 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700441 helper, cl = GetGerrit(opts, cl)
442 helper.SetReviewers(cl, add=add_list, remove=remove_list,
443 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700444
445
Allen Li38abdaa2017-03-16 13:25:02 -0700446def UserActAssign(opts, cl, assignee):
447 """Set assignee for CL <n>"""
448 helper, cl = GetGerrit(opts, cl)
449 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
450
451
Mike Frysinger88f27292014-06-17 09:40:45 -0700452def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530453 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700454 helper, cl = GetGerrit(opts, cl)
455 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530456
457
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800458def UserActTopic(opts, topic, *args):
459 """Set |topic| for CL number <n> [n ...]"""
460 for arg in args:
461 helper, arg = GetGerrit(opts, arg)
462 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
463
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700464def UserActPrivate(opts, cl, private_str):
465 """Set private bit on CL to private"""
466 try:
467 private = cros_build_lib.BooleanShellValue(private_str, False)
468 except ValueError:
469 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
470
471 helper, cl = GetGerrit(opts, cl)
472 helper.SetPrivate(cl, private)
473
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800474
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800475def UserActSethashtags(opts, cl, *args):
476 """Add/remove hashtags for CL <n> (prepend with '~' to remove)"""
477 hashtags = args
478 add = []
479 remove = []
480 for hashtag in hashtags:
481 if hashtag.startswith('~'):
482 remove.append(hashtag[1:])
483 else:
484 add.append(hashtag)
485 helper, cl = GetGerrit(opts, cl)
486 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
487
488
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700489def UserActDeletedraft(opts, *args):
Marc Herbert02448c82015-10-07 14:03:34 -0700490 """Delete draft CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700491 for arg in args:
492 helper, cl = GetGerrit(opts, arg)
493 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800494
495
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800496def UserActAccount(opts):
497 """Get user account information."""
498 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400499 acct = helper.GetAccount()
500 if opts.json:
501 json.dump(acct, sys.stdout)
502 else:
503 print('account_id:%i %s <%s>' %
504 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800505
506
Mike Frysinger108eda22018-06-06 18:45:12 -0400507def GetParser():
508 """Returns the parser to use for this module."""
509 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400510
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500511 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400512
513There is no support for doing line-by-line code review via the command line.
514This helps you manage various bits and CL status.
515
Mike Frysingera1db2c42014-06-15 00:42:48 -0700516For general Gerrit documentation, see:
517 https://gerrit-review.googlesource.com/Documentation/
518The Searching Changes page covers the search query syntax:
519 https://gerrit-review.googlesource.com/Documentation/user-search.html
520
Mike Frysinger13f23a42013-05-13 17:32:01 -0400521Example:
522 $ gerrit todo # List all the CLs that await your review.
523 $ gerrit mine # List all of your open CLs.
524 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
525 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
526 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700527Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700528 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
529ready.
530 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
531ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400532 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400533
534Actions:"""
Mike Frysinger108eda22018-06-06 18:45:12 -0400535 indent = max([len(x) - len(ACTION_PREFIX) for x in actions])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400536 for a in sorted(actions):
Mike Frysinger108eda22018-06-06 18:45:12 -0400537 cmd = a[len(ACTION_PREFIX):]
Mike Frysinger15b23e42014-12-05 17:00:05 -0500538 # Sanity check for devs adding new commands. Should be quick.
539 if cmd != cmd.lower().capitalize():
540 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
541 (cmd, cmd.lower().capitalize()))
542 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400543
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500544 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500545 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700546 default=site_config.params.EXTERNAL_GOB_INSTANCE,
547 const=site_config.params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500548 help='Query internal Chromium Gerrit instance')
549 parser.add_argument('-g', '--gob',
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700550 default=site_config.params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500551 help=('Gerrit (on borg) instance to query (default: %s)' %
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700552 (site_config.params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500553 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400554 help='Key to sort on (number, project); use "unsorted" '
555 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700556 parser.add_argument('--raw', default=False, action='store_true',
557 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400558 parser.add_argument('--json', default=False, action='store_true',
559 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700560 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
561 dest='dryrun',
562 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500563 parser.add_argument('-v', '--verbose', default=False, action='store_true',
564 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800565 parser.add_argument('-b', '--branch',
566 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700567 parser.add_argument('--draft', default=False, action='store_true',
568 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800569 parser.add_argument('-p', '--project',
570 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800571 parser.add_argument('-t', '--topic',
572 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500573 parser.add_argument('action', help='The gerrit action to perform')
574 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400575
576 return parser
577
578
579def main(argv):
580 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500581 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400582
Mike Frysinger88f27292014-06-17 09:40:45 -0700583 # A cache of gerrit helpers we'll load on demand.
584 opts.gerrit = {}
585 opts.Freeze()
586
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400587 # pylint: disable=W0603
588 global COLOR
589 COLOR = terminal.Color(enabled=opts.color)
590
Mike Frysinger13f23a42013-05-13 17:32:01 -0400591 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400592 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400593 if functor:
594 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700595 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700596 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500597 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700598 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500599 (opts.action, arg_min))
600 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400601 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500602 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700603 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500604 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500605 except (cros_build_lib.RunCommandError, gerrit.GerritException,
606 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700607 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400608 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500609 parser.error('unknown action: %s' % (opts.action,))