blob: 2b70fbafa4639bb5878529226fd6dd11bcd9fc88 [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 Frysinger31ff6f92014-02-08 04:33:03 -050012from __future__ import print_function
13
Mike Frysinger13f23a42013-05-13 17:32:01 -040014import inspect
15import os
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -080016import pprint
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 ',],
David James2b2e2c52014-12-02 19:32:07 -080034 'TRY': ['T ', 'Trybot Ready ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080035 '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
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800169 if opts.branch is not None:
170 query += ' branch:%s' % opts.branch
171
Mike Frysinger88f27292014-06-17 09:40:45 -0700172 helper, _ = GetGerrit(opts)
173 for cl in helper.Query(query, raw=True, bypass_cache=False):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400174 # Gerrit likes to return a stats record too.
175 if not 'project' in cl:
176 continue
177
178 # Strip off common leading names since the result is still
179 # unique over the whole tree.
180 if not opts.verbose:
Mike Frysingere5e78272014-06-15 00:41:30 -0700181 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform',
182 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400183 if cl['project'].startswith('%s/' % pfx):
184 cl['project'] = cl['project'][len(pfx) + 1:]
185
186 ret.append(cl)
187
188 if opts.sort in ('number',):
189 key = lambda x: int(x[opts.sort])
190 else:
191 key = lambda x: x[opts.sort]
192 return sorted(ret, key=key)
193
194
Mike Frysinger13f23a42013-05-13 17:32:01 -0400195def IsApprover(cl, users):
196 """See if the approvers in |cl| is listed in |users|"""
197 # See if we are listed in the approvals list. We have to parse
198 # this by hand as the gerrit query system doesn't support it :(
199 # http://code.google.com/p/gerrit/issues/detail?id=1235
200 if 'approvals' not in cl['currentPatchSet']:
201 return False
202
203 if isinstance(users, basestring):
204 users = (users,)
205
206 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700207 if (approver['by']['email'] in users and
208 approver['type'] == 'CRVW' and
209 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400210 return True
211
212 return False
213
214
215def UserActTodo(opts):
216 """List CLs needing your review"""
217 emails, reviewers, owners = _MyUserInfo()
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500218 cls = FilteredQuery(opts, ('( %s ) status:open NOT ( %s )' %
219 (' OR '.join(reviewers), ' OR '.join(owners))))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400220 cls = [x for x in cls if not IsApprover(x, emails)]
221 lims = limits(cls)
222 for cl in cls:
223 PrintCl(opts, cl, lims)
224
225
Mike Frysingera1db2c42014-06-15 00:42:48 -0700226def UserActSearch(opts, query):
227 """List CLs matching the Gerrit <search query>"""
228 cls = FilteredQuery(opts, query)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400229 lims = limits(cls)
230 for cl in cls:
231 PrintCl(opts, cl, lims)
232
233
Mike Frysingera1db2c42014-06-15 00:42:48 -0700234def UserActMine(opts):
235 """List your CLs with review statuses"""
236 _, _, owners = _MyUserInfo()
237 UserActSearch(opts, '( %s ) status:new' % (' OR '.join(owners),))
238
239
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700240def UserActInspect(opts, *args):
241 """Inspect CL number <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700242 for arg in args:
243 cl = FilteredQuery(opts, arg)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700244 if cl:
245 PrintCl(opts, cl[0], None)
246 else:
Mike Frysinger88f27292014-06-17 09:40:45 -0700247 print('no results found for CL %s' % arg)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400248
249
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700250def UserActReview(opts, *args):
251 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
252 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700253 for arg in args[:-1]:
254 helper, cl = GetGerrit(opts, arg)
255 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700256UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400257
258
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700259def UserActVerify(opts, *args):
260 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
261 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700262 for arg in args[:-1]:
263 helper, cl = GetGerrit(opts, arg)
264 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700265UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400266
267
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700268def UserActReady(opts, *args):
269 """Mark CL <n> [n ...] with ready status <0,1,2>"""
270 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700271 for arg in args[:-1]:
272 helper, cl = GetGerrit(opts, arg)
273 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700274UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400275
276
Mike Frysinger15b23e42014-12-05 17:00:05 -0500277def UserActTrybotready(opts, *args):
278 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
279 num = args[-1]
280 for arg in args[:-1]:
281 helper, cl = GetGerrit(opts, arg)
282 helper.SetReview(cl, labels={'Trybot-Ready': num}, dryrun=opts.dryrun)
283UserActTrybotready.arg_min = 2
284
285
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700286def UserActSubmit(opts, *args):
287 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700288 for arg in args:
289 helper, cl = GetGerrit(opts, arg)
290 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400291
292
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700293def UserActAbandon(opts, *args):
294 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700295 for arg in args:
296 helper, cl = GetGerrit(opts, arg)
297 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400298
299
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700300def UserActRestore(opts, *args):
301 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700302 for arg in args:
303 helper, cl = GetGerrit(opts, arg)
304 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400305
306
Mike Frysinger88f27292014-06-17 09:40:45 -0700307def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700308 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500309 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700310 # Allow for optional leading '~'.
311 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
312 add_list, remove_list, invalid_list = [], [], []
313
314 for x in emails:
315 if not email_validator.match(x):
316 invalid_list.append(x)
317 elif x[0] == '~':
318 remove_list.append(x[1:])
319 else:
320 add_list.append(x)
321
322 if invalid_list:
323 cros_build_lib.Die(
324 'Invalid email address(es): %s' % ', '.join(invalid_list))
325
326 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700327 helper, cl = GetGerrit(opts, cl)
328 helper.SetReviewers(cl, add=add_list, remove=remove_list,
329 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700330
331
Mike Frysinger88f27292014-06-17 09:40:45 -0700332def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530333 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700334 helper, cl = GetGerrit(opts, cl)
335 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530336
337
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700338def UserActDeletedraft(opts, *args):
339 """Delete draft patch set <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700340 for arg in args:
341 helper, cl = GetGerrit(opts, arg)
342 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800343
344
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800345def UserActAccount(opts):
346 """Get user account information."""
347 helper, _ = GetGerrit(opts)
348 pprint.PrettyPrinter().pprint(helper.GetAccount())
349
350
Mike Frysinger13f23a42013-05-13 17:32:01 -0400351def main(argv):
352 # Locate actions that are exposed to the user. All functions that start
353 # with "UserAct" are fair game.
354 act_pfx = 'UserAct'
355 actions = [x for x in globals() if x.startswith(act_pfx)]
356
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500357 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400358
359There is no support for doing line-by-line code review via the command line.
360This helps you manage various bits and CL status.
361
Mike Frysingera1db2c42014-06-15 00:42:48 -0700362For general Gerrit documentation, see:
363 https://gerrit-review.googlesource.com/Documentation/
364The Searching Changes page covers the search query syntax:
365 https://gerrit-review.googlesource.com/Documentation/user-search.html
366
Mike Frysinger13f23a42013-05-13 17:32:01 -0400367Example:
368 $ gerrit todo # List all the CLs that await your review.
369 $ gerrit mine # List all of your open CLs.
370 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
371 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
372 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700373Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700374 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
375ready.
376 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
377ready.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400378
379Actions:"""
380 indent = max([len(x) - len(act_pfx) for x in actions])
381 for a in sorted(actions):
Mike Frysinger15b23e42014-12-05 17:00:05 -0500382 cmd = a[len(act_pfx):]
383 # Sanity check for devs adding new commands. Should be quick.
384 if cmd != cmd.lower().capitalize():
385 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
386 (cmd, cmd.lower().capitalize()))
387 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400388
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500389 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500390 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Mike Frysinger88f27292014-06-17 09:40:45 -0700391 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysinger40541c62014-02-08 04:38:37 -0500392 const=constants.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500393 help='Query internal Chromium Gerrit instance')
394 parser.add_argument('-g', '--gob',
Mike Frysinger88f27292014-06-17 09:40:45 -0700395 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500396 help=('Gerrit (on borg) instance to query (default: %s)' %
397 (constants.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500398 parser.add_argument('--sort', default='number',
399 help='Key to sort on (number, project)')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700400 parser.add_argument('--raw', default=False, action='store_true',
401 help='Return raw results (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700402 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
403 dest='dryrun',
404 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500405 parser.add_argument('-v', '--verbose', default=False, action='store_true',
406 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800407 parser.add_argument('-b', '--branch',
408 help='Limit output to the specific branch')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500409 parser.add_argument('args', nargs='+')
410 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400411
Mike Frysinger88f27292014-06-17 09:40:45 -0700412 # A cache of gerrit helpers we'll load on demand.
413 opts.gerrit = {}
414 opts.Freeze()
415
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400416 # pylint: disable=W0603
417 global COLOR
418 COLOR = terminal.Color(enabled=opts.color)
419
Mike Frysinger13f23a42013-05-13 17:32:01 -0400420 # Now look up the requested user action and run it.
Mike Frysinger88f27292014-06-17 09:40:45 -0700421 cmd = opts.args[0].lower()
422 args = opts.args[1:]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400423 functor = globals().get(act_pfx + cmd.capitalize())
424 if functor:
425 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700426 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700427 arg_min = getattr(functor, 'arg_min', len(argspec.args))
428 if len(args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700429 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700430 (cmd, arg_min))
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700431 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400432 parser.error('incorrect number of args: %s expects %s' %
433 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700434 try:
435 functor(opts, *args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500436 except (cros_build_lib.RunCommandError, gerrit.GerritException,
437 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700438 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400439 else:
440 parser.error('unknown action: %s' % (cmd,))