blob: 96e4388953e292b9d286266894bf02b2fc4623dd [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 Frysinger031ad0b2013-05-14 18:15:34 -040034COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040035
36# Map the internal names to the ones we normally show on the web ui.
37GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080038 'COMR': ['CQ', 'Commit Queue ',],
39 'CRVW': ['CR', 'Code Review ',],
40 'SUBM': ['S ', 'Submitted ',],
David James2b2e2c52014-12-02 19:32:07 -080041 'TRY': ['T ', 'Trybot Ready ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080042 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040043}
44
45# Order is important -- matches the web ui. This also controls the short
46# entries that we summarize in non-verbose mode.
47GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
48
49
50def red(s):
51 return COLOR.Color(terminal.Color.RED, s)
52
53
54def green(s):
55 return COLOR.Color(terminal.Color.GREEN, s)
56
57
58def blue(s):
59 return COLOR.Color(terminal.Color.BLUE, s)
60
61
62def limits(cls):
63 """Given a dict of fields, calculate the longest string lengths
64
65 This allows you to easily format the output of many results so that the
66 various cols all line up correctly.
67 """
68 lims = {}
69 for cl in cls:
70 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040071 # Use %s rather than str() to avoid codec issues.
72 # We also do this so we can format integers.
73 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040074 return lims
75
76
Mike Frysinger88f27292014-06-17 09:40:45 -070077# TODO: This func really needs to be merged into the core gerrit logic.
78def GetGerrit(opts, cl=None):
79 """Auto pick the right gerrit instance based on the |cl|
80
81 Args:
82 opts: The general options object.
83 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
84
85 Returns:
86 A tuple of a gerrit object and a sanitized CL #.
87 """
88 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -070089 if cl is not None:
Mike Frysinger88f27292014-06-17 09:40:45 -070090 if cl.startswith('*'):
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -070091 gob = site_config.params.INTERNAL_GOB_INSTANCE
Mike Frysinger88f27292014-06-17 09:40:45 -070092 cl = cl[1:]
93 elif ':' in cl:
94 gob, cl = cl.split(':', 1)
95
96 if not gob in opts.gerrit:
97 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
98
99 return (opts.gerrit[gob], cl)
100
101
Mike Frysinger13f23a42013-05-13 17:32:01 -0400102def GetApprovalSummary(_opts, cls):
103 """Return a dict of the most important approvals"""
104 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
105 if 'approvals' in cls['currentPatchSet']:
106 for approver in cls['currentPatchSet']['approvals']:
107 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
108 if not cats:
Ralph Nathan446aee92015-03-23 14:44:56 -0700109 logging.warning('unknown gerrit approval type: %s', approver['type'])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400110 continue
111 cat = cats[0].strip()
112 val = int(approver['value'])
113 if not cat in approvs:
114 # Ignore the extended categories in the summary view.
115 continue
Mike Frysingera0313d02017-07-10 16:44:43 -0400116 elif approvs[cat] == '':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400117 approvs[cat] = val
118 elif val < 0:
119 approvs[cat] = min(approvs[cat], val)
120 else:
121 approvs[cat] = max(approvs[cat], val)
122 return approvs
123
124
Mike Frysingera1b4b272017-04-05 16:11:00 -0400125def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400126 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400127 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400128 lims = {'url': 0, 'project': 0}
129
130 status = ''
131 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400132 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400133 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400134 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400135 functor = lambda x: x
136 elif approvs[cat] < 0:
137 functor = red
138 else:
139 functor = green
140 status += functor('%s:%2s ' % (cat, approvs[cat]))
141
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400142 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
143 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400144
145 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400146 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400147 functor = red if int(approver['value']) < 0 else green
148 n = functor('%2s' % approver['value'])
149 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
150 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500151 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400152
153
Mike Frysingera1b4b272017-04-05 16:11:00 -0400154def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400155 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400156 if opts.raw:
157 pfx = ''
158 # Special case internal Chrome GoB as that is what most devs use.
159 # They can always redirect the list elsewhere via the -g option.
160 if opts.gob == site_config.params.INTERNAL_GOB_INSTANCE:
161 pfx = site_config.params.INTERNAL_CHANGE_PREFIX
162 for cl in cls:
163 print('%s%s' % (pfx, cl['number']))
164
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400165 elif opts.json:
166 json.dump(cls, sys.stdout)
167
Mike Frysingera1b4b272017-04-05 16:11:00 -0400168 else:
169 if lims is None:
170 lims = limits(cls)
171
172 for cl in cls:
173 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
174
175
Mike Frysinger13f23a42013-05-13 17:32:01 -0400176def _MyUserInfo():
Mike Frysinger2cd56022017-01-12 20:56:27 -0500177 """Try to return e-mail addresses used by the active user."""
178 return [git.GetProjectUserEmail(constants.CHROMITE_DIR)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400179
180
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400181def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700182 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800183 if opts.branch is not None:
184 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800185 if opts.project is not None:
186 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800187 if opts.topic is not None:
188 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800189
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400190 if helper is None:
191 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700192 return helper.Query(query, raw=raw, bypass_cache=False)
193
194
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400195def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700196 """Query gerrit and filter/clean up the results"""
197 ret = []
198
Mike Frysinger2cd56022017-01-12 20:56:27 -0500199 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400200 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400201 # Gerrit likes to return a stats record too.
202 if not 'project' in cl:
203 continue
204
205 # Strip off common leading names since the result is still
206 # unique over the whole tree.
207 if not opts.verbose:
Mike Frysingerc0fc8de2017-04-04 17:49:27 -0400208 for pfx in ('chromeos', 'chromiumos', 'external', 'overlays', 'platform',
Mike Frysingere5e78272014-06-15 00:41:30 -0700209 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400210 if cl['project'].startswith('%s/' % pfx):
211 cl['project'] = cl['project'][len(pfx) + 1:]
212
213 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 IsApprover(cl, users):
225 """See if the approvers in |cl| is listed in |users|"""
226 # See if we are listed in the approvals list. We have to parse
227 # this by hand as the gerrit query system doesn't support it :(
228 # http://code.google.com/p/gerrit/issues/detail?id=1235
229 if 'approvals' not in cl['currentPatchSet']:
230 return False
231
232 if isinstance(users, basestring):
233 users = (users,)
234
235 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700236 if (approver['by']['email'] in users and
237 approver['type'] == 'CRVW' and
238 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400239 return True
240
241 return False
242
243
244def UserActTodo(opts):
245 """List CLs needing your review"""
Mike Frysinger2cd56022017-01-12 20:56:27 -0500246 emails = _MyUserInfo()
247 cls = FilteredQuery(opts, 'reviewer:self status:open NOT owner:self')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400248 cls = [x for x in cls if not IsApprover(x, emails)]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400249 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400250
251
Mike Frysingera1db2c42014-06-15 00:42:48 -0700252def UserActSearch(opts, query):
253 """List CLs matching the Gerrit <search query>"""
254 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400255 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400256
257
Mike Frysingera1db2c42014-06-15 00:42:48 -0700258def UserActMine(opts):
259 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700260 if opts.draft:
261 rule = 'is:draft'
262 else:
263 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500264 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700265
266
Paul Hobbs89765232015-06-24 14:07:49 -0700267def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
268 """Runs breadth first search starting from the nodes in |to_visit|
269
270 Args:
271 to_visit: the starting nodes
272 children: a function which takes a node and returns the nodes adjacent to it
273 visited_key: a function for deduplicating node visits. Defaults to the
274 identity function (lambda x: x)
275
276 Returns:
277 A list of nodes which are reachable from any node in |to_visit| by calling
278 |children| any number of times.
279 """
280 to_visit = list(to_visit)
281 seen = set(map(visited_key, to_visit))
282 for node in to_visit:
283 for child in children(node):
284 key = visited_key(child)
285 if key not in seen:
286 seen.add(key)
287 to_visit.append(child)
288 return to_visit
289
290
291def UserActDeps(opts, query):
292 """List CLs matching a query, and all transitive dependencies of those CLs"""
293 cls = _Query(opts, query, raw=False)
294
295 @cros_build_lib.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400296 def _QueryChange(cl, helper=None):
297 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700298
Mike Frysinger5726da92017-09-20 22:14:25 -0400299 def _ProcessDeps(cl, deps, required):
300 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700301 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400302 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400303 if not dep.remote in opts.gerrit:
304 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
305 remote=dep.remote, print_cmd=opts.debug)
306 helper = opts.gerrit[dep.remote]
307
Paul Hobbs89765232015-06-24 14:07:49 -0700308 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400309 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
310
311 # Handle empty results. If we found a commit that was pushed directly
312 # (e.g. a bot commit), then gerrit won't know about it.
313 if not changes:
314 if required:
315 logging.error('CL %s depends on %s which cannot be found',
316 cl, dep.ToGerritQueryText())
317 continue
318
319 # Our query might have matched more than one result. This can come up
320 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
321 # across multiple repos/branches. We blindly check all of them in the
322 # hopes that all open ones are what the user wants, but then again the
323 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
324 if len(changes) > 1:
325 logging.warning('CL %s has an ambiguous CQ dependency %s',
326 cl, dep.ToGerritQueryText())
327 for change in changes:
328 if change.status == 'NEW':
329 yield change
330
331 def _Children(cl):
332 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
333 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
334 yield change
335 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
336 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700337
338 transitives = _BreadthFirstSearch(
339 cls, _Children,
340 visited_key=lambda cl: cl.gerrit_number)
341
342 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400343 PrintCls(opts, transitives_raw)
Paul Hobbs89765232015-06-24 14:07:49 -0700344
345
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700346def UserActInspect(opts, *args):
347 """Inspect CL number <n> [n ...]"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400348 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700349 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400350 helper, cl = GetGerrit(opts, arg)
351 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
352 if change:
353 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700354 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400355 logging.warning('no results found for CL %s', arg)
356 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400357
358
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700359def UserActReview(opts, *args):
360 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
361 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700362 for arg in args[:-1]:
363 helper, cl = GetGerrit(opts, arg)
364 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700365UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400366
367
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700368def UserActVerify(opts, *args):
369 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
370 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700371 for arg in args[:-1]:
372 helper, cl = GetGerrit(opts, arg)
373 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700374UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400375
376
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700377def UserActReady(opts, *args):
Kirtika Ruchandanica852f42017-05-23 18:18:05 -0700378 """Mark CL <n> [n ...] with ready status <0,1>"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700379 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700380 for arg in args[:-1]:
381 helper, cl = GetGerrit(opts, arg)
382 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700383UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400384
385
Mike Frysinger15b23e42014-12-05 17:00:05 -0500386def UserActTrybotready(opts, *args):
387 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
388 num = args[-1]
389 for arg in args[:-1]:
390 helper, cl = GetGerrit(opts, arg)
391 helper.SetReview(cl, labels={'Trybot-Ready': num}, dryrun=opts.dryrun)
392UserActTrybotready.arg_min = 2
393
394
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700395def UserActSubmit(opts, *args):
396 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700397 for arg in args:
398 helper, cl = GetGerrit(opts, arg)
399 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400400
401
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700402def UserActAbandon(opts, *args):
403 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700404 for arg in args:
405 helper, cl = GetGerrit(opts, arg)
406 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400407
408
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700409def UserActRestore(opts, *args):
410 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700411 for arg in args:
412 helper, cl = GetGerrit(opts, arg)
413 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400414
415
Mike Frysinger88f27292014-06-17 09:40:45 -0700416def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700417 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500418 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700419 # Allow for optional leading '~'.
420 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
421 add_list, remove_list, invalid_list = [], [], []
422
423 for x in emails:
424 if not email_validator.match(x):
425 invalid_list.append(x)
426 elif x[0] == '~':
427 remove_list.append(x[1:])
428 else:
429 add_list.append(x)
430
431 if invalid_list:
432 cros_build_lib.Die(
433 'Invalid email address(es): %s' % ', '.join(invalid_list))
434
435 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700436 helper, cl = GetGerrit(opts, cl)
437 helper.SetReviewers(cl, add=add_list, remove=remove_list,
438 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700439
440
Allen Li38abdaa2017-03-16 13:25:02 -0700441def UserActAssign(opts, cl, assignee):
442 """Set assignee for CL <n>"""
443 helper, cl = GetGerrit(opts, cl)
444 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
445
446
Mike Frysinger88f27292014-06-17 09:40:45 -0700447def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530448 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700449 helper, cl = GetGerrit(opts, cl)
450 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530451
452
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800453def UserActTopic(opts, topic, *args):
454 """Set |topic| for CL number <n> [n ...]"""
455 for arg in args:
456 helper, arg = GetGerrit(opts, arg)
457 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
458
459
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800460def UserActSethashtags(opts, cl, *args):
461 """Add/remove hashtags for CL <n> (prepend with '~' to remove)"""
462 hashtags = args
463 add = []
464 remove = []
465 for hashtag in hashtags:
466 if hashtag.startswith('~'):
467 remove.append(hashtag[1:])
468 else:
469 add.append(hashtag)
470 helper, cl = GetGerrit(opts, cl)
471 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
472
473
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700474def UserActDeletedraft(opts, *args):
Marc Herbert02448c82015-10-07 14:03:34 -0700475 """Delete draft CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700476 for arg in args:
477 helper, cl = GetGerrit(opts, arg)
478 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800479
480
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800481def UserActAccount(opts):
482 """Get user account information."""
483 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400484 acct = helper.GetAccount()
485 if opts.json:
486 json.dump(acct, sys.stdout)
487 else:
488 print('account_id:%i %s <%s>' %
489 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800490
491
Mike Frysinger13f23a42013-05-13 17:32:01 -0400492def main(argv):
493 # Locate actions that are exposed to the user. All functions that start
494 # with "UserAct" are fair game.
495 act_pfx = 'UserAct'
496 actions = [x for x in globals() if x.startswith(act_pfx)]
497
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500498 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400499
500There is no support for doing line-by-line code review via the command line.
501This helps you manage various bits and CL status.
502
Mike Frysingera1db2c42014-06-15 00:42:48 -0700503For general Gerrit documentation, see:
504 https://gerrit-review.googlesource.com/Documentation/
505The Searching Changes page covers the search query syntax:
506 https://gerrit-review.googlesource.com/Documentation/user-search.html
507
Mike Frysinger13f23a42013-05-13 17:32:01 -0400508Example:
509 $ gerrit todo # List all the CLs that await your review.
510 $ gerrit mine # List all of your open CLs.
511 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
512 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
513 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700514Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700515 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
516ready.
517 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
518ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400519 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400520
521Actions:"""
522 indent = max([len(x) - len(act_pfx) for x in actions])
523 for a in sorted(actions):
Mike Frysinger15b23e42014-12-05 17:00:05 -0500524 cmd = a[len(act_pfx):]
525 # Sanity check for devs adding new commands. Should be quick.
526 if cmd != cmd.lower().capitalize():
527 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
528 (cmd, cmd.lower().capitalize()))
529 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400530
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500531 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500532 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700533 default=site_config.params.EXTERNAL_GOB_INSTANCE,
534 const=site_config.params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500535 help='Query internal Chromium Gerrit instance')
536 parser.add_argument('-g', '--gob',
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700537 default=site_config.params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500538 help=('Gerrit (on borg) instance to query (default: %s)' %
Matthew Sartorid2e6bdf2015-07-23 12:07:39 -0700539 (site_config.params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500540 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400541 help='Key to sort on (number, project); use "unsorted" '
542 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700543 parser.add_argument('--raw', default=False, action='store_true',
544 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400545 parser.add_argument('--json', default=False, action='store_true',
546 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700547 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
548 dest='dryrun',
549 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500550 parser.add_argument('-v', '--verbose', default=False, action='store_true',
551 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800552 parser.add_argument('-b', '--branch',
553 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700554 parser.add_argument('--draft', default=False, action='store_true',
555 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800556 parser.add_argument('-p', '--project',
557 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800558 parser.add_argument('-t', '--topic',
559 help='Limit output to the specific topic')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500560 parser.add_argument('args', nargs='+')
561 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400562
Mike Frysinger88f27292014-06-17 09:40:45 -0700563 # A cache of gerrit helpers we'll load on demand.
564 opts.gerrit = {}
565 opts.Freeze()
566
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400567 # pylint: disable=W0603
568 global COLOR
569 COLOR = terminal.Color(enabled=opts.color)
570
Mike Frysinger13f23a42013-05-13 17:32:01 -0400571 # Now look up the requested user action and run it.
Mike Frysinger88f27292014-06-17 09:40:45 -0700572 cmd = opts.args[0].lower()
573 args = opts.args[1:]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400574 functor = globals().get(act_pfx + cmd.capitalize())
575 if functor:
576 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700577 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700578 arg_min = getattr(functor, 'arg_min', len(argspec.args))
579 if len(args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700580 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700581 (cmd, arg_min))
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700582 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400583 parser.error('incorrect number of args: %s expects %s' %
584 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700585 try:
586 functor(opts, *args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500587 except (cros_build_lib.RunCommandError, gerrit.GerritException,
588 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700589 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400590 else:
591 parser.error('unknown action: %s' % (cmd,))