blob: 06a45286a9a62dbeffdbcd087ce17a673344f0ab [file] [log] [blame]
Mike Frysinger13f23a42013-05-13 17:32:01 -04001#!/usr/bin/python
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
16import os
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070017import re
Mike Frysinger13f23a42013-05-13 17:32:01 -040018
Don Garrett88b8d782014-05-13 17:30:55 -070019from chromite.cbuildbot import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040020from chromite.lib import commandline
21from chromite.lib import cros_build_lib
22from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050023from chromite.lib import gob_util
Mike Frysinger13f23a42013-05-13 17:32:01 -040024from chromite.lib import terminal
25
26
Mike Frysinger031ad0b2013-05-14 18:15:34 -040027COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040028
29# Map the internal names to the ones we normally show on the web ui.
30GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080031 'COMR': ['CQ', 'Commit Queue ',],
32 'CRVW': ['CR', 'Code Review ',],
33 'SUBM': ['S ', 'Submitted ',],
34 'TBVF': ['TV', 'Trybot Verified',],
35 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040036}
37
38# Order is important -- matches the web ui. This also controls the short
39# entries that we summarize in non-verbose mode.
40GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
41
42
43def red(s):
44 return COLOR.Color(terminal.Color.RED, s)
45
46
47def green(s):
48 return COLOR.Color(terminal.Color.GREEN, s)
49
50
51def blue(s):
52 return COLOR.Color(terminal.Color.BLUE, s)
53
54
55def limits(cls):
56 """Given a dict of fields, calculate the longest string lengths
57
58 This allows you to easily format the output of many results so that the
59 various cols all line up correctly.
60 """
61 lims = {}
62 for cl in cls:
63 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040064 # Use %s rather than str() to avoid codec issues.
65 # We also do this so we can format integers.
66 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040067 return lims
68
69
Mike Frysinger88f27292014-06-17 09:40:45 -070070# TODO: This func really needs to be merged into the core gerrit logic.
71def GetGerrit(opts, cl=None):
72 """Auto pick the right gerrit instance based on the |cl|
73
74 Args:
75 opts: The general options object.
76 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
77
78 Returns:
79 A tuple of a gerrit object and a sanitized CL #.
80 """
81 gob = opts.gob
82 if not cl is None:
83 if cl.startswith('*'):
84 gob = constants.INTERNAL_GOB_INSTANCE
85 cl = cl[1:]
86 elif ':' in cl:
87 gob, cl = cl.split(':', 1)
88
89 if not gob in opts.gerrit:
90 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
91
92 return (opts.gerrit[gob], cl)
93
94
Mike Frysinger13f23a42013-05-13 17:32:01 -040095def GetApprovalSummary(_opts, cls):
96 """Return a dict of the most important approvals"""
97 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
98 if 'approvals' in cls['currentPatchSet']:
99 for approver in cls['currentPatchSet']['approvals']:
100 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
101 if not cats:
102 cros_build_lib.Warning('unknown gerrit approval type: %s',
103 approver['type'])
104 continue
105 cat = cats[0].strip()
106 val = int(approver['value'])
107 if not cat in approvs:
108 # Ignore the extended categories in the summary view.
109 continue
110 elif approvs[cat] is '':
111 approvs[cat] = val
112 elif val < 0:
113 approvs[cat] = min(approvs[cat], val)
114 else:
115 approvs[cat] = max(approvs[cat], val)
116 return approvs
117
118
119def PrintCl(opts, cls, lims, show_approvals=True):
120 """Pretty print a single result"""
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700121 if opts.raw:
122 # Special case internal Chrome GoB as that is what most devs use.
123 # They can always redirect the list elsewhere via the -g option.
124 if opts.gob == constants.INTERNAL_GOB_INSTANCE:
125 print(constants.INTERNAL_CHANGE_PREFIX, end='')
126 print(cls['number'])
127 return
128
Mike Frysinger13f23a42013-05-13 17:32:01 -0400129 if not lims:
130 lims = {'url': 0, 'project': 0}
131
132 status = ''
133 if show_approvals and not opts.verbose:
134 approvs = GetApprovalSummary(opts, cls)
135 for cat in GERRIT_SUMMARY_CATS:
136 if approvs[cat] is '':
137 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 Frysinger31ff6f92014-02-08 04:33:03 -0500144 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cls['url'])), status,
145 lims['project'], cls['project'], cls['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400146
147 if show_approvals and opts.verbose:
148 for approver in cls['currentPatchSet'].get('approvals', []):
149 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
156def _MyUserInfo():
157 username = os.environ['USER']
158 emails = ['%s@%s' % (username, domain)
159 for domain in ('google.com', 'chromium.org')]
160 reviewers = ['reviewer:%s' % x for x in emails]
161 owners = ['owner:%s' % x for x in emails]
162 return emails, reviewers, owners
163
164
165def FilteredQuery(opts, query):
166 """Query gerrit and filter/clean up the results"""
167 ret = []
168
Mike Frysinger88f27292014-06-17 09:40:45 -0700169 helper, _ = GetGerrit(opts)
170 for cl in helper.Query(query, raw=True, bypass_cache=False):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400171 # Gerrit likes to return a stats record too.
172 if not 'project' in cl:
173 continue
174
175 # Strip off common leading names since the result is still
176 # unique over the whole tree.
177 if not opts.verbose:
Mike Frysingere5e78272014-06-15 00:41:30 -0700178 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform',
179 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400180 if cl['project'].startswith('%s/' % pfx):
181 cl['project'] = cl['project'][len(pfx) + 1:]
182
183 ret.append(cl)
184
185 if opts.sort in ('number',):
186 key = lambda x: int(x[opts.sort])
187 else:
188 key = lambda x: x[opts.sort]
189 return sorted(ret, key=key)
190
191
Mike Frysinger13f23a42013-05-13 17:32:01 -0400192def IsApprover(cl, users):
193 """See if the approvers in |cl| is listed in |users|"""
194 # See if we are listed in the approvals list. We have to parse
195 # this by hand as the gerrit query system doesn't support it :(
196 # http://code.google.com/p/gerrit/issues/detail?id=1235
197 if 'approvals' not in cl['currentPatchSet']:
198 return False
199
200 if isinstance(users, basestring):
201 users = (users,)
202
203 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700204 if (approver['by']['email'] in users and
205 approver['type'] == 'CRVW' and
206 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400207 return True
208
209 return False
210
211
212def UserActTodo(opts):
213 """List CLs needing your review"""
214 emails, reviewers, owners = _MyUserInfo()
215 cls = FilteredQuery(opts, '( %s ) status:open NOT ( %s )' %
216 (' OR '.join(reviewers), ' OR '.join(owners)))
217 cls = [x for x in cls if not IsApprover(x, emails)]
218 lims = limits(cls)
219 for cl in cls:
220 PrintCl(opts, cl, lims)
221
222
Mike Frysingera1db2c42014-06-15 00:42:48 -0700223def UserActSearch(opts, query):
224 """List CLs matching the Gerrit <search query>"""
225 cls = FilteredQuery(opts, query)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400226 lims = limits(cls)
227 for cl in cls:
228 PrintCl(opts, cl, lims)
229
230
Mike Frysingera1db2c42014-06-15 00:42:48 -0700231def UserActMine(opts):
232 """List your CLs with review statuses"""
233 _, _, owners = _MyUserInfo()
234 UserActSearch(opts, '( %s ) status:new' % (' OR '.join(owners),))
235
236
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700237def UserActInspect(opts, *args):
238 """Inspect CL number <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700239 for arg in args:
240 cl = FilteredQuery(opts, arg)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700241 if cl:
242 PrintCl(opts, cl[0], None)
243 else:
Mike Frysinger88f27292014-06-17 09:40:45 -0700244 print('no results found for CL %s' % arg)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400245
246
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700247def UserActReview(opts, *args):
248 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
249 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700250 for arg in args[:-1]:
251 helper, cl = GetGerrit(opts, arg)
252 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700253UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400254
255
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700256def UserActVerify(opts, *args):
257 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
258 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700259 for arg in args[:-1]:
260 helper, cl = GetGerrit(opts, arg)
261 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700262UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400263
264
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700265def UserActReady(opts, *args):
266 """Mark CL <n> [n ...] with ready status <0,1,2>"""
267 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700268 for arg in args[:-1]:
269 helper, cl = GetGerrit(opts, arg)
270 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700271UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400272
273
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700274def UserActSubmit(opts, *args):
275 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700276 for arg in args:
277 helper, cl = GetGerrit(opts, arg)
278 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400279
280
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700281def UserActAbandon(opts, *args):
282 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700283 for arg in args:
284 helper, cl = GetGerrit(opts, arg)
285 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400286
287
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700288def UserActRestore(opts, *args):
289 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700290 for arg in args:
291 helper, cl = GetGerrit(opts, arg)
292 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400293
294
Mike Frysinger88f27292014-06-17 09:40:45 -0700295def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700296 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500297 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700298 # Allow for optional leading '~'.
299 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
300 add_list, remove_list, invalid_list = [], [], []
301
302 for x in emails:
303 if not email_validator.match(x):
304 invalid_list.append(x)
305 elif x[0] == '~':
306 remove_list.append(x[1:])
307 else:
308 add_list.append(x)
309
310 if invalid_list:
311 cros_build_lib.Die(
312 'Invalid email address(es): %s' % ', '.join(invalid_list))
313
314 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700315 helper, cl = GetGerrit(opts, cl)
316 helper.SetReviewers(cl, add=add_list, remove=remove_list,
317 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700318
319
Mike Frysinger88f27292014-06-17 09:40:45 -0700320def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530321 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700322 helper, cl = GetGerrit(opts, cl)
323 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530324
325
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700326def UserActDeletedraft(opts, *args):
327 """Delete draft patch set <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700328 for arg in args:
329 helper, cl = GetGerrit(opts, arg)
330 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800331
332
Mike Frysinger13f23a42013-05-13 17:32:01 -0400333def main(argv):
334 # Locate actions that are exposed to the user. All functions that start
335 # with "UserAct" are fair game.
336 act_pfx = 'UserAct'
337 actions = [x for x in globals() if x.startswith(act_pfx)]
338
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500339 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400340
341There is no support for doing line-by-line code review via the command line.
342This helps you manage various bits and CL status.
343
Mike Frysingera1db2c42014-06-15 00:42:48 -0700344For general Gerrit documentation, see:
345 https://gerrit-review.googlesource.com/Documentation/
346The Searching Changes page covers the search query syntax:
347 https://gerrit-review.googlesource.com/Documentation/user-search.html
348
Mike Frysinger13f23a42013-05-13 17:32:01 -0400349Example:
350 $ gerrit todo # List all the CLs that await your review.
351 $ gerrit mine # List all of your open CLs.
352 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
353 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
354 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700355Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700356 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
357ready.
358 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
359ready.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400360
361Actions:"""
362 indent = max([len(x) - len(act_pfx) for x in actions])
363 for a in sorted(actions):
364 usage += '\n %-*s: %s' % (indent, a[len(act_pfx):].lower(),
365 globals()[a].__doc__)
366
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500367 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500368 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Mike Frysinger88f27292014-06-17 09:40:45 -0700369 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysinger40541c62014-02-08 04:38:37 -0500370 const=constants.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500371 help='Query internal Chromium Gerrit instance')
372 parser.add_argument('-g', '--gob',
Mike Frysinger88f27292014-06-17 09:40:45 -0700373 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500374 help='Gerrit (on borg) instance to query (default: %s)' %
Mike Frysinger40541c62014-02-08 04:38:37 -0500375 (constants.EXTERNAL_GOB_INSTANCE))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500376 parser.add_argument('--sort', default='number',
377 help='Key to sort on (number, project)')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700378 parser.add_argument('--raw', default=False, action='store_true',
379 help='Return raw results (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700380 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
381 dest='dryrun',
382 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500383 parser.add_argument('-v', '--verbose', default=False, action='store_true',
384 help='Be more verbose in output')
385 parser.add_argument('args', nargs='+')
386 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400387
Mike Frysinger88f27292014-06-17 09:40:45 -0700388 # A cache of gerrit helpers we'll load on demand.
389 opts.gerrit = {}
390 opts.Freeze()
391
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400392 # pylint: disable=W0603
393 global COLOR
394 COLOR = terminal.Color(enabled=opts.color)
395
Mike Frysinger13f23a42013-05-13 17:32:01 -0400396 # Now look up the requested user action and run it.
Mike Frysinger88f27292014-06-17 09:40:45 -0700397 cmd = opts.args[0].lower()
398 args = opts.args[1:]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400399 functor = globals().get(act_pfx + cmd.capitalize())
400 if functor:
401 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700402 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700403 arg_min = getattr(functor, 'arg_min', len(argspec.args))
404 if len(args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700405 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700406 (cmd, arg_min))
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700407 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400408 parser.error('incorrect number of args: %s expects %s' %
409 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700410 try:
411 functor(opts, *args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500412 except (cros_build_lib.RunCommandError, gerrit.GerritException,
413 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700414 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400415 else:
416 parser.error('unknown action: %s' % (cmd,))