blob: 525c6b9a564ba41c6475766690f63772fae20bed [file] [log] [blame]
Mike Frysinger13f23a42013-05-13 17:32:01 -04001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Mike Frysinger08737512014-02-07 22:58:26 -05005"""A command line interface to Gerrit-on-borg instances.
Mike Frysinger13f23a42013-05-13 17:32:01 -04006
7Internal Note:
8To expose a function directly to the command line interface, name your function
9with the prefix "UserAct".
10"""
11
Mike Frysinger1d4752b2014-11-08 04:00:18 -050012# pylint: disable=bad-continuation
13
Mike Frysinger31ff6f92014-02-08 04:33:03 -050014from __future__ import print_function
15
Mike Frysinger13f23a42013-05-13 17:32:01 -040016import inspect
17import os
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -080018import pprint
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070019import re
Mike Frysinger13f23a42013-05-13 17:32:01 -040020
Don Garrett88b8d782014-05-13 17:30:55 -070021from chromite.cbuildbot import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040022from chromite.lib import commandline
23from chromite.lib import cros_build_lib
24from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050025from chromite.lib import gob_util
Mike Frysinger13f23a42013-05-13 17:32:01 -040026from chromite.lib import terminal
27
28
Mike Frysinger031ad0b2013-05-14 18:15:34 -040029COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040030
31# Map the internal names to the ones we normally show on the web ui.
32GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080033 'COMR': ['CQ', 'Commit Queue ',],
34 'CRVW': ['CR', 'Code Review ',],
35 'SUBM': ['S ', 'Submitted ',],
36 'TBVF': ['TV', 'Trybot Verified',],
37 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040038}
39
40# Order is important -- matches the web ui. This also controls the short
41# entries that we summarize in non-verbose mode.
42GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
43
44
45def red(s):
46 return COLOR.Color(terminal.Color.RED, s)
47
48
49def green(s):
50 return COLOR.Color(terminal.Color.GREEN, s)
51
52
53def blue(s):
54 return COLOR.Color(terminal.Color.BLUE, s)
55
56
57def limits(cls):
58 """Given a dict of fields, calculate the longest string lengths
59
60 This allows you to easily format the output of many results so that the
61 various cols all line up correctly.
62 """
63 lims = {}
64 for cl in cls:
65 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040066 # Use %s rather than str() to avoid codec issues.
67 # We also do this so we can format integers.
68 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040069 return lims
70
71
Mike Frysinger88f27292014-06-17 09:40:45 -070072# TODO: This func really needs to be merged into the core gerrit logic.
73def GetGerrit(opts, cl=None):
74 """Auto pick the right gerrit instance based on the |cl|
75
76 Args:
77 opts: The general options object.
78 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
79
80 Returns:
81 A tuple of a gerrit object and a sanitized CL #.
82 """
83 gob = opts.gob
84 if not cl is None:
85 if cl.startswith('*'):
86 gob = constants.INTERNAL_GOB_INSTANCE
87 cl = cl[1:]
88 elif ':' in cl:
89 gob, cl = cl.split(':', 1)
90
91 if not gob in opts.gerrit:
92 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
93
94 return (opts.gerrit[gob], cl)
95
96
Mike Frysinger13f23a42013-05-13 17:32:01 -040097def GetApprovalSummary(_opts, cls):
98 """Return a dict of the most important approvals"""
99 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
100 if 'approvals' in cls['currentPatchSet']:
101 for approver in cls['currentPatchSet']['approvals']:
102 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
103 if not cats:
104 cros_build_lib.Warning('unknown gerrit approval type: %s',
105 approver['type'])
106 continue
107 cat = cats[0].strip()
108 val = int(approver['value'])
109 if not cat in approvs:
110 # Ignore the extended categories in the summary view.
111 continue
112 elif approvs[cat] is '':
113 approvs[cat] = val
114 elif val < 0:
115 approvs[cat] = min(approvs[cat], val)
116 else:
117 approvs[cat] = max(approvs[cat], val)
118 return approvs
119
120
121def PrintCl(opts, cls, lims, show_approvals=True):
122 """Pretty print a single result"""
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700123 if opts.raw:
124 # Special case internal Chrome GoB as that is what most devs use.
125 # They can always redirect the list elsewhere via the -g option.
126 if opts.gob == constants.INTERNAL_GOB_INSTANCE:
127 print(constants.INTERNAL_CHANGE_PREFIX, end='')
128 print(cls['number'])
129 return
130
Mike Frysinger13f23a42013-05-13 17:32:01 -0400131 if not lims:
132 lims = {'url': 0, 'project': 0}
133
134 status = ''
135 if show_approvals and not opts.verbose:
136 approvs = GetApprovalSummary(opts, cls)
137 for cat in GERRIT_SUMMARY_CATS:
138 if approvs[cat] is '':
139 functor = lambda x: x
140 elif approvs[cat] < 0:
141 functor = red
142 else:
143 functor = green
144 status += functor('%s:%2s ' % (cat, approvs[cat]))
145
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500146 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cls['url'])), status,
147 lims['project'], cls['project'], cls['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400148
149 if show_approvals and opts.verbose:
150 for approver in cls['currentPatchSet'].get('approvals', []):
151 functor = red if int(approver['value']) < 0 else green
152 n = functor('%2s' % approver['value'])
153 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
154 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500155 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400156
157
158def _MyUserInfo():
159 username = os.environ['USER']
160 emails = ['%s@%s' % (username, domain)
161 for domain in ('google.com', 'chromium.org')]
162 reviewers = ['reviewer:%s' % x for x in emails]
163 owners = ['owner:%s' % x for x in emails]
164 return emails, reviewers, owners
165
166
167def FilteredQuery(opts, query):
168 """Query gerrit and filter/clean up the results"""
169 ret = []
170
Mike Frysinger88f27292014-06-17 09:40:45 -0700171 helper, _ = GetGerrit(opts)
172 for cl in helper.Query(query, raw=True, bypass_cache=False):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400173 # Gerrit likes to return a stats record too.
174 if not 'project' in cl:
175 continue
176
177 # Strip off common leading names since the result is still
178 # unique over the whole tree.
179 if not opts.verbose:
Mike Frysingere5e78272014-06-15 00:41:30 -0700180 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform',
181 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400182 if cl['project'].startswith('%s/' % pfx):
183 cl['project'] = cl['project'][len(pfx) + 1:]
184
185 ret.append(cl)
186
187 if opts.sort in ('number',):
188 key = lambda x: int(x[opts.sort])
189 else:
190 key = lambda x: x[opts.sort]
191 return sorted(ret, key=key)
192
193
Mike Frysinger13f23a42013-05-13 17:32:01 -0400194def IsApprover(cl, users):
195 """See if the approvers in |cl| is listed in |users|"""
196 # See if we are listed in the approvals list. We have to parse
197 # this by hand as the gerrit query system doesn't support it :(
198 # http://code.google.com/p/gerrit/issues/detail?id=1235
199 if 'approvals' not in cl['currentPatchSet']:
200 return False
201
202 if isinstance(users, basestring):
203 users = (users,)
204
205 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700206 if (approver['by']['email'] in users and
207 approver['type'] == 'CRVW' and
208 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400209 return True
210
211 return False
212
213
214def UserActTodo(opts):
215 """List CLs needing your review"""
216 emails, reviewers, owners = _MyUserInfo()
217 cls = FilteredQuery(opts, '( %s ) status:open NOT ( %s )' %
218 (' OR '.join(reviewers), ' OR '.join(owners)))
219 cls = [x for x in cls if not IsApprover(x, emails)]
220 lims = limits(cls)
221 for cl in cls:
222 PrintCl(opts, cl, lims)
223
224
Mike Frysingera1db2c42014-06-15 00:42:48 -0700225def UserActSearch(opts, query):
226 """List CLs matching the Gerrit <search query>"""
227 cls = FilteredQuery(opts, query)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400228 lims = limits(cls)
229 for cl in cls:
230 PrintCl(opts, cl, lims)
231
232
Mike Frysingera1db2c42014-06-15 00:42:48 -0700233def UserActMine(opts):
234 """List your CLs with review statuses"""
235 _, _, owners = _MyUserInfo()
236 UserActSearch(opts, '( %s ) status:new' % (' OR '.join(owners),))
237
238
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700239def UserActInspect(opts, *args):
240 """Inspect CL number <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700241 for arg in args:
242 cl = FilteredQuery(opts, arg)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700243 if cl:
244 PrintCl(opts, cl[0], None)
245 else:
Mike Frysinger88f27292014-06-17 09:40:45 -0700246 print('no results found for CL %s' % arg)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400247
248
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700249def UserActReview(opts, *args):
250 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
251 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700252 for arg in args[:-1]:
253 helper, cl = GetGerrit(opts, arg)
254 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700255UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400256
257
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700258def UserActVerify(opts, *args):
259 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
260 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700261 for arg in args[:-1]:
262 helper, cl = GetGerrit(opts, arg)
263 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700264UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400265
266
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700267def UserActReady(opts, *args):
268 """Mark CL <n> [n ...] with ready status <0,1,2>"""
269 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700270 for arg in args[:-1]:
271 helper, cl = GetGerrit(opts, arg)
272 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700273UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400274
275
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700276def UserActSubmit(opts, *args):
277 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700278 for arg in args:
279 helper, cl = GetGerrit(opts, arg)
280 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400281
282
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700283def UserActAbandon(opts, *args):
284 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700285 for arg in args:
286 helper, cl = GetGerrit(opts, arg)
287 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400288
289
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700290def UserActRestore(opts, *args):
291 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700292 for arg in args:
293 helper, cl = GetGerrit(opts, arg)
294 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400295
296
Mike Frysinger88f27292014-06-17 09:40:45 -0700297def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700298 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500299 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700300 # Allow for optional leading '~'.
301 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
302 add_list, remove_list, invalid_list = [], [], []
303
304 for x in emails:
305 if not email_validator.match(x):
306 invalid_list.append(x)
307 elif x[0] == '~':
308 remove_list.append(x[1:])
309 else:
310 add_list.append(x)
311
312 if invalid_list:
313 cros_build_lib.Die(
314 'Invalid email address(es): %s' % ', '.join(invalid_list))
315
316 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700317 helper, cl = GetGerrit(opts, cl)
318 helper.SetReviewers(cl, add=add_list, remove=remove_list,
319 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700320
321
Mike Frysinger88f27292014-06-17 09:40:45 -0700322def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530323 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700324 helper, cl = GetGerrit(opts, cl)
325 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530326
327
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700328def UserActDeletedraft(opts, *args):
329 """Delete draft patch set <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700330 for arg in args:
331 helper, cl = GetGerrit(opts, arg)
332 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800333
334
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800335def UserActAccount(opts):
336 """Get user account information."""
337 helper, _ = GetGerrit(opts)
338 pprint.PrettyPrinter().pprint(helper.GetAccount())
339
340
Mike Frysinger13f23a42013-05-13 17:32:01 -0400341def main(argv):
342 # Locate actions that are exposed to the user. All functions that start
343 # with "UserAct" are fair game.
344 act_pfx = 'UserAct'
345 actions = [x for x in globals() if x.startswith(act_pfx)]
346
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500347 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400348
349There is no support for doing line-by-line code review via the command line.
350This helps you manage various bits and CL status.
351
Mike Frysingera1db2c42014-06-15 00:42:48 -0700352For general Gerrit documentation, see:
353 https://gerrit-review.googlesource.com/Documentation/
354The Searching Changes page covers the search query syntax:
355 https://gerrit-review.googlesource.com/Documentation/user-search.html
356
Mike Frysinger13f23a42013-05-13 17:32:01 -0400357Example:
358 $ gerrit todo # List all the CLs that await your review.
359 $ gerrit mine # List all of your open CLs.
360 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
361 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
362 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700363Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700364 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
365ready.
366 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
367ready.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400368
369Actions:"""
370 indent = max([len(x) - len(act_pfx) for x in actions])
371 for a in sorted(actions):
372 usage += '\n %-*s: %s' % (indent, a[len(act_pfx):].lower(),
373 globals()[a].__doc__)
374
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500375 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500376 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Mike Frysinger88f27292014-06-17 09:40:45 -0700377 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysinger40541c62014-02-08 04:38:37 -0500378 const=constants.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500379 help='Query internal Chromium Gerrit instance')
380 parser.add_argument('-g', '--gob',
Mike Frysinger88f27292014-06-17 09:40:45 -0700381 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500382 help='Gerrit (on borg) instance to query (default: %s)' %
Mike Frysinger40541c62014-02-08 04:38:37 -0500383 (constants.EXTERNAL_GOB_INSTANCE))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500384 parser.add_argument('--sort', default='number',
385 help='Key to sort on (number, project)')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700386 parser.add_argument('--raw', default=False, action='store_true',
387 help='Return raw results (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700388 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
389 dest='dryrun',
390 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500391 parser.add_argument('-v', '--verbose', default=False, action='store_true',
392 help='Be more verbose in output')
393 parser.add_argument('args', nargs='+')
394 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400395
Mike Frysinger88f27292014-06-17 09:40:45 -0700396 # A cache of gerrit helpers we'll load on demand.
397 opts.gerrit = {}
398 opts.Freeze()
399
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400400 # pylint: disable=W0603
401 global COLOR
402 COLOR = terminal.Color(enabled=opts.color)
403
Mike Frysinger13f23a42013-05-13 17:32:01 -0400404 # Now look up the requested user action and run it.
Mike Frysinger88f27292014-06-17 09:40:45 -0700405 cmd = opts.args[0].lower()
406 args = opts.args[1:]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400407 functor = globals().get(act_pfx + cmd.capitalize())
408 if functor:
409 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700410 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700411 arg_min = getattr(functor, 'arg_min', len(argspec.args))
412 if len(args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700413 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700414 (cmd, arg_min))
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700415 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400416 parser.error('incorrect number of args: %s expects %s' %
417 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700418 try:
419 functor(opts, *args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500420 except (cros_build_lib.RunCommandError, gerrit.GerritException,
421 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700422 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400423 else:
424 parser.error('unknown action: %s' % (cmd,))