blob: 3e20b2170da379510a36efae7bc7d70535933cfe [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 '
227 'label:Code-Review=0,user=self'))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400228 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400229
230
Mike Frysingera1db2c42014-06-15 00:42:48 -0700231def UserActSearch(opts, query):
232 """List CLs matching the Gerrit <search query>"""
233 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400234 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400235
236
Mike Frysingera1db2c42014-06-15 00:42:48 -0700237def UserActMine(opts):
238 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700239 if opts.draft:
240 rule = 'is:draft'
241 else:
242 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500243 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700244
245
Paul Hobbs89765232015-06-24 14:07:49 -0700246def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
247 """Runs breadth first search starting from the nodes in |to_visit|
248
249 Args:
250 to_visit: the starting nodes
251 children: a function which takes a node and returns the nodes adjacent to it
252 visited_key: a function for deduplicating node visits. Defaults to the
253 identity function (lambda x: x)
254
255 Returns:
256 A list of nodes which are reachable from any node in |to_visit| by calling
257 |children| any number of times.
258 """
259 to_visit = list(to_visit)
260 seen = set(map(visited_key, to_visit))
261 for node in to_visit:
262 for child in children(node):
263 key = visited_key(child)
264 if key not in seen:
265 seen.add(key)
266 to_visit.append(child)
267 return to_visit
268
269
270def UserActDeps(opts, query):
271 """List CLs matching a query, and all transitive dependencies of those CLs"""
272 cls = _Query(opts, query, raw=False)
273
Mike Frysinger10666292018-07-12 01:03:38 -0400274 @memoize.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400275 def _QueryChange(cl, helper=None):
276 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700277
Mike Frysinger5726da92017-09-20 22:14:25 -0400278 def _ProcessDeps(cl, deps, required):
279 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700280 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400281 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400282 if not dep.remote in opts.gerrit:
283 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
284 remote=dep.remote, print_cmd=opts.debug)
285 helper = opts.gerrit[dep.remote]
286
Paul Hobbs89765232015-06-24 14:07:49 -0700287 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400288 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
289
290 # Handle empty results. If we found a commit that was pushed directly
291 # (e.g. a bot commit), then gerrit won't know about it.
292 if not changes:
293 if required:
294 logging.error('CL %s depends on %s which cannot be found',
295 cl, dep.ToGerritQueryText())
296 continue
297
298 # Our query might have matched more than one result. This can come up
299 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
300 # across multiple repos/branches. We blindly check all of them in the
301 # hopes that all open ones are what the user wants, but then again the
302 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
303 if len(changes) > 1:
304 logging.warning('CL %s has an ambiguous CQ dependency %s',
305 cl, dep.ToGerritQueryText())
306 for change in changes:
307 if change.status == 'NEW':
308 yield change
309
310 def _Children(cl):
311 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
312 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
313 yield change
314 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
315 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700316
317 transitives = _BreadthFirstSearch(
318 cls, _Children,
319 visited_key=lambda cl: cl.gerrit_number)
320
321 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400322 PrintCls(opts, transitives_raw)
Paul Hobbs89765232015-06-24 14:07:49 -0700323
324
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700325def UserActInspect(opts, *args):
326 """Inspect CL number <n> [n ...]"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400327 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700328 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400329 helper, cl = GetGerrit(opts, arg)
330 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
331 if change:
332 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700333 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400334 logging.warning('no results found for CL %s', arg)
335 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400336
337
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700338def UserActReview(opts, *args):
339 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
340 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700341 for arg in args[:-1]:
342 helper, cl = GetGerrit(opts, arg)
343 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700344UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400345
346
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700347def UserActVerify(opts, *args):
348 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
349 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700350 for arg in args[:-1]:
351 helper, cl = GetGerrit(opts, arg)
352 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700353UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400354
355
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700356def UserActReady(opts, *args):
Kirtika Ruchandanica852f42017-05-23 18:18:05 -0700357 """Mark CL <n> [n ...] with ready status <0,1>"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700358 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700359 for arg in args[:-1]:
360 helper, cl = GetGerrit(opts, arg)
361 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700362UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400363
364
Mike Frysinger15b23e42014-12-05 17:00:05 -0500365def UserActTrybotready(opts, *args):
366 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
367 num = args[-1]
368 for arg in args[:-1]:
369 helper, cl = GetGerrit(opts, arg)
370 helper.SetReview(cl, labels={'Trybot-Ready': num}, dryrun=opts.dryrun)
371UserActTrybotready.arg_min = 2
372
373
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700374def UserActSubmit(opts, *args):
375 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700376 for arg in args:
377 helper, cl = GetGerrit(opts, arg)
378 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400379
380
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700381def UserActAbandon(opts, *args):
382 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700383 for arg in args:
384 helper, cl = GetGerrit(opts, arg)
385 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400386
387
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700388def UserActRestore(opts, *args):
389 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700390 for arg in args:
391 helper, cl = GetGerrit(opts, arg)
392 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400393
394
Mike Frysinger88f27292014-06-17 09:40:45 -0700395def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700396 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500397 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700398 # Allow for optional leading '~'.
399 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
400 add_list, remove_list, invalid_list = [], [], []
401
402 for x in emails:
403 if not email_validator.match(x):
404 invalid_list.append(x)
405 elif x[0] == '~':
406 remove_list.append(x[1:])
407 else:
408 add_list.append(x)
409
410 if invalid_list:
411 cros_build_lib.Die(
412 'Invalid email address(es): %s' % ', '.join(invalid_list))
413
414 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700415 helper, cl = GetGerrit(opts, cl)
416 helper.SetReviewers(cl, add=add_list, remove=remove_list,
417 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700418
419
Allen Li38abdaa2017-03-16 13:25:02 -0700420def UserActAssign(opts, cl, assignee):
421 """Set assignee for CL <n>"""
422 helper, cl = GetGerrit(opts, cl)
423 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
424
425
Mike Frysinger88f27292014-06-17 09:40:45 -0700426def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530427 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700428 helper, cl = GetGerrit(opts, cl)
429 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530430
431
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800432def UserActTopic(opts, topic, *args):
433 """Set |topic| for CL number <n> [n ...]"""
434 for arg in args:
435 helper, arg = GetGerrit(opts, arg)
436 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
437
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700438def UserActPrivate(opts, cl, private_str):
439 """Set private bit on CL to private"""
440 try:
441 private = cros_build_lib.BooleanShellValue(private_str, False)
442 except ValueError:
443 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
444
445 helper, cl = GetGerrit(opts, cl)
446 helper.SetPrivate(cl, private)
447
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800448
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800449def UserActSethashtags(opts, cl, *args):
450 """Add/remove hashtags for CL <n> (prepend with '~' to remove)"""
451 hashtags = args
452 add = []
453 remove = []
454 for hashtag in hashtags:
455 if hashtag.startswith('~'):
456 remove.append(hashtag[1:])
457 else:
458 add.append(hashtag)
459 helper, cl = GetGerrit(opts, cl)
460 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
461
462
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700463def UserActDeletedraft(opts, *args):
Marc Herbert02448c82015-10-07 14:03:34 -0700464 """Delete draft CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700465 for arg in args:
466 helper, cl = GetGerrit(opts, arg)
467 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800468
469
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800470def UserActAccount(opts):
471 """Get user account information."""
472 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400473 acct = helper.GetAccount()
474 if opts.json:
475 json.dump(acct, sys.stdout)
476 else:
477 print('account_id:%i %s <%s>' %
478 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800479
480
Mike Frysinger108eda22018-06-06 18:45:12 -0400481def GetParser():
482 """Returns the parser to use for this module."""
483 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400484
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500485 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400486
487There is no support for doing line-by-line code review via the command line.
488This helps you manage various bits and CL status.
489
Mike Frysingera1db2c42014-06-15 00:42:48 -0700490For general Gerrit documentation, see:
491 https://gerrit-review.googlesource.com/Documentation/
492The Searching Changes page covers the search query syntax:
493 https://gerrit-review.googlesource.com/Documentation/user-search.html
494
Mike Frysinger13f23a42013-05-13 17:32:01 -0400495Example:
496 $ gerrit todo # List all the CLs that await your review.
497 $ gerrit mine # List all of your open CLs.
498 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
499 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
500 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700501Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700502 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
503ready.
504 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
505ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400506 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400507
508Actions:"""
Mike Frysinger108eda22018-06-06 18:45:12 -0400509 indent = max([len(x) - len(ACTION_PREFIX) for x in actions])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400510 for a in sorted(actions):
Mike Frysinger108eda22018-06-06 18:45:12 -0400511 cmd = a[len(ACTION_PREFIX):]
Mike Frysinger15b23e42014-12-05 17:00:05 -0500512 # Sanity check for devs adding new commands. Should be quick.
513 if cmd != cmd.lower().capitalize():
514 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
515 (cmd, cmd.lower().capitalize()))
516 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400517
Alex Klein2ab29cc2018-07-19 12:01:00 -0600518 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500519 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500520 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600521 default=site_params.EXTERNAL_GOB_INSTANCE,
522 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500523 help='Query internal Chromium Gerrit instance')
524 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600525 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500526 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600527 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500528 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400529 help='Key to sort on (number, project); use "unsorted" '
530 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700531 parser.add_argument('--raw', default=False, action='store_true',
532 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400533 parser.add_argument('--json', default=False, action='store_true',
534 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700535 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
536 dest='dryrun',
537 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500538 parser.add_argument('-v', '--verbose', default=False, action='store_true',
539 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800540 parser.add_argument('-b', '--branch',
541 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700542 parser.add_argument('--draft', default=False, action='store_true',
543 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800544 parser.add_argument('-p', '--project',
545 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800546 parser.add_argument('-t', '--topic',
547 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500548 parser.add_argument('action', help='The gerrit action to perform')
549 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400550
551 return parser
552
553
554def main(argv):
555 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500556 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400557
Mike Frysinger88f27292014-06-17 09:40:45 -0700558 # A cache of gerrit helpers we'll load on demand.
559 opts.gerrit = {}
560 opts.Freeze()
561
Mike Frysinger27e21b72018-07-12 14:20:21 -0400562 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400563 global COLOR
564 COLOR = terminal.Color(enabled=opts.color)
565
Mike Frysinger13f23a42013-05-13 17:32:01 -0400566 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400567 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400568 if functor:
569 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700570 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700571 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500572 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700573 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500574 (opts.action, arg_min))
575 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400576 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500577 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700578 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500579 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500580 except (cros_build_lib.RunCommandError, gerrit.GerritException,
581 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700582 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400583 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500584 parser.error('unknown action: %s' % (opts.action,))