blob: 0b742de5e7c5e6012eee2c5aa5b04d0fe1b7a917 [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)
344 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700345UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400346
347
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700348def UserActVerify(opts, *args):
349 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
350 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700351 for arg in args[:-1]:
352 helper, cl = GetGerrit(opts, arg)
353 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700354UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400355
356
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700357def UserActReady(opts, *args):
Kirtika Ruchandanica852f42017-05-23 18:18:05 -0700358 """Mark CL <n> [n ...] with ready status <0,1>"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700359 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700360 for arg in args[:-1]:
361 helper, cl = GetGerrit(opts, arg)
362 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700363UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400364
365
Mike Frysinger15b23e42014-12-05 17:00:05 -0500366def UserActTrybotready(opts, *args):
367 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
368 num = args[-1]
369 for arg in args[:-1]:
370 helper, cl = GetGerrit(opts, arg)
371 helper.SetReview(cl, labels={'Trybot-Ready': num}, dryrun=opts.dryrun)
372UserActTrybotready.arg_min = 2
373
374
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700375def UserActSubmit(opts, *args):
376 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700377 for arg in args:
378 helper, cl = GetGerrit(opts, arg)
379 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400380
381
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700382def UserActAbandon(opts, *args):
383 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700384 for arg in args:
385 helper, cl = GetGerrit(opts, arg)
386 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400387
388
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700389def UserActRestore(opts, *args):
390 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700391 for arg in args:
392 helper, cl = GetGerrit(opts, arg)
393 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400394
395
Mike Frysinger88f27292014-06-17 09:40:45 -0700396def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700397 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500398 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700399 # Allow for optional leading '~'.
400 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
401 add_list, remove_list, invalid_list = [], [], []
402
403 for x in emails:
404 if not email_validator.match(x):
405 invalid_list.append(x)
406 elif x[0] == '~':
407 remove_list.append(x[1:])
408 else:
409 add_list.append(x)
410
411 if invalid_list:
412 cros_build_lib.Die(
413 'Invalid email address(es): %s' % ', '.join(invalid_list))
414
415 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700416 helper, cl = GetGerrit(opts, cl)
417 helper.SetReviewers(cl, add=add_list, remove=remove_list,
418 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700419
420
Allen Li38abdaa2017-03-16 13:25:02 -0700421def UserActAssign(opts, cl, assignee):
422 """Set assignee for CL <n>"""
423 helper, cl = GetGerrit(opts, cl)
424 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
425
426
Mike Frysinger88f27292014-06-17 09:40:45 -0700427def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530428 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700429 helper, cl = GetGerrit(opts, cl)
430 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530431
432
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800433def UserActTopic(opts, topic, *args):
434 """Set |topic| for CL number <n> [n ...]"""
435 for arg in args:
436 helper, arg = GetGerrit(opts, arg)
437 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
438
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700439def UserActPrivate(opts, cl, private_str):
440 """Set private bit on CL to private"""
441 try:
442 private = cros_build_lib.BooleanShellValue(private_str, False)
443 except ValueError:
444 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
445
446 helper, cl = GetGerrit(opts, cl)
447 helper.SetPrivate(cl, private)
448
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800449
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800450def UserActSethashtags(opts, cl, *args):
451 """Add/remove hashtags for CL <n> (prepend with '~' to remove)"""
452 hashtags = args
453 add = []
454 remove = []
455 for hashtag in hashtags:
456 if hashtag.startswith('~'):
457 remove.append(hashtag[1:])
458 else:
459 add.append(hashtag)
460 helper, cl = GetGerrit(opts, cl)
461 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
462
463
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700464def UserActDeletedraft(opts, *args):
Marc Herbert02448c82015-10-07 14:03:34 -0700465 """Delete draft CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700466 for arg in args:
467 helper, cl = GetGerrit(opts, arg)
468 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800469
470
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800471def UserActAccount(opts):
472 """Get user account information."""
473 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400474 acct = helper.GetAccount()
475 if opts.json:
476 json.dump(acct, sys.stdout)
477 else:
478 print('account_id:%i %s <%s>' %
479 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800480
481
Mike Frysinger108eda22018-06-06 18:45:12 -0400482def GetParser():
483 """Returns the parser to use for this module."""
484 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400485
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500486 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400487
488There is no support for doing line-by-line code review via the command line.
489This helps you manage various bits and CL status.
490
Mike Frysingera1db2c42014-06-15 00:42:48 -0700491For general Gerrit documentation, see:
492 https://gerrit-review.googlesource.com/Documentation/
493The Searching Changes page covers the search query syntax:
494 https://gerrit-review.googlesource.com/Documentation/user-search.html
495
Mike Frysinger13f23a42013-05-13 17:32:01 -0400496Example:
497 $ gerrit todo # List all the CLs that await your review.
498 $ gerrit mine # List all of your open CLs.
499 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
500 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
501 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700502Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700503 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
504ready.
505 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
506ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400507 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400508
509Actions:"""
Mike Frysinger108eda22018-06-06 18:45:12 -0400510 indent = max([len(x) - len(ACTION_PREFIX) for x in actions])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400511 for a in sorted(actions):
Mike Frysinger108eda22018-06-06 18:45:12 -0400512 cmd = a[len(ACTION_PREFIX):]
Mike Frysinger15b23e42014-12-05 17:00:05 -0500513 # Sanity check for devs adding new commands. Should be quick.
514 if cmd != cmd.lower().capitalize():
515 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
516 (cmd, cmd.lower().capitalize()))
517 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400518
Alex Klein2ab29cc2018-07-19 12:01:00 -0600519 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500520 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500521 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600522 default=site_params.EXTERNAL_GOB_INSTANCE,
523 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500524 help='Query internal Chromium Gerrit instance')
525 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600526 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500527 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600528 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500529 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400530 help='Key to sort on (number, project); use "unsorted" '
531 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700532 parser.add_argument('--raw', default=False, action='store_true',
533 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400534 parser.add_argument('--json', default=False, action='store_true',
535 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700536 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
537 dest='dryrun',
538 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500539 parser.add_argument('-v', '--verbose', default=False, action='store_true',
540 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800541 parser.add_argument('-b', '--branch',
542 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700543 parser.add_argument('--draft', default=False, action='store_true',
544 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800545 parser.add_argument('-p', '--project',
546 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800547 parser.add_argument('-t', '--topic',
548 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500549 parser.add_argument('action', help='The gerrit action to perform')
550 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400551
552 return parser
553
554
555def main(argv):
556 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500557 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400558
Mike Frysinger88f27292014-06-17 09:40:45 -0700559 # A cache of gerrit helpers we'll load on demand.
560 opts.gerrit = {}
561 opts.Freeze()
562
Mike Frysinger27e21b72018-07-12 14:20:21 -0400563 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400564 global COLOR
565 COLOR = terminal.Color(enabled=opts.color)
566
Mike Frysinger13f23a42013-05-13 17:32:01 -0400567 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400568 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400569 if functor:
570 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700571 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700572 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500573 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700574 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500575 (opts.action, arg_min))
576 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400577 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500578 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700579 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500580 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500581 except (cros_build_lib.RunCommandError, gerrit.GerritException,
582 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700583 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400584 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500585 parser.error('unknown action: %s' % (opts.action,))