blob: 8e1b824aa996e316c41880d79a98be17ce274e7f [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
70def GetApprovalSummary(_opts, cls):
71 """Return a dict of the most important approvals"""
72 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
73 if 'approvals' in cls['currentPatchSet']:
74 for approver in cls['currentPatchSet']['approvals']:
75 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
76 if not cats:
77 cros_build_lib.Warning('unknown gerrit approval type: %s',
78 approver['type'])
79 continue
80 cat = cats[0].strip()
81 val = int(approver['value'])
82 if not cat in approvs:
83 # Ignore the extended categories in the summary view.
84 continue
85 elif approvs[cat] is '':
86 approvs[cat] = val
87 elif val < 0:
88 approvs[cat] = min(approvs[cat], val)
89 else:
90 approvs[cat] = max(approvs[cat], val)
91 return approvs
92
93
94def PrintCl(opts, cls, lims, show_approvals=True):
95 """Pretty print a single result"""
Mike Frysingerf70bdc72014-06-15 00:44:06 -070096 if opts.raw:
97 # Special case internal Chrome GoB as that is what most devs use.
98 # They can always redirect the list elsewhere via the -g option.
99 if opts.gob == constants.INTERNAL_GOB_INSTANCE:
100 print(constants.INTERNAL_CHANGE_PREFIX, end='')
101 print(cls['number'])
102 return
103
Mike Frysinger13f23a42013-05-13 17:32:01 -0400104 if not lims:
105 lims = {'url': 0, 'project': 0}
106
107 status = ''
108 if show_approvals and not opts.verbose:
109 approvs = GetApprovalSummary(opts, cls)
110 for cat in GERRIT_SUMMARY_CATS:
111 if approvs[cat] is '':
112 functor = lambda x: x
113 elif approvs[cat] < 0:
114 functor = red
115 else:
116 functor = green
117 status += functor('%s:%2s ' % (cat, approvs[cat]))
118
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500119 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cls['url'])), status,
120 lims['project'], cls['project'], cls['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400121
122 if show_approvals and opts.verbose:
123 for approver in cls['currentPatchSet'].get('approvals', []):
124 functor = red if int(approver['value']) < 0 else green
125 n = functor('%2s' % approver['value'])
126 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
127 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500128 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400129
130
131def _MyUserInfo():
132 username = os.environ['USER']
133 emails = ['%s@%s' % (username, domain)
134 for domain in ('google.com', 'chromium.org')]
135 reviewers = ['reviewer:%s' % x for x in emails]
136 owners = ['owner:%s' % x for x in emails]
137 return emails, reviewers, owners
138
139
140def FilteredQuery(opts, query):
141 """Query gerrit and filter/clean up the results"""
142 ret = []
143
Mike Frysinger20fac842014-06-15 00:40:00 -0700144 for cl in opts.gerrit.Query(query, raw=True, bypass_cache=False):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400145 # Gerrit likes to return a stats record too.
146 if not 'project' in cl:
147 continue
148
149 # Strip off common leading names since the result is still
150 # unique over the whole tree.
151 if not opts.verbose:
Mike Frysingere5e78272014-06-15 00:41:30 -0700152 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform',
153 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400154 if cl['project'].startswith('%s/' % pfx):
155 cl['project'] = cl['project'][len(pfx) + 1:]
156
157 ret.append(cl)
158
159 if opts.sort in ('number',):
160 key = lambda x: int(x[opts.sort])
161 else:
162 key = lambda x: x[opts.sort]
163 return sorted(ret, key=key)
164
165
166def ChangeNumberToCommit(opts, idx):
167 """Given a gerrit CL #, return the revision info
168
169 This is the form that the gerrit ssh interface expects.
170 """
171 cl = opts.gerrit.QuerySingleRecord(idx, raw=True)
172 return cl['currentPatchSet']['revision']
173
174
Mike Frysinger13f23a42013-05-13 17:32:01 -0400175def IsApprover(cl, users):
176 """See if the approvers in |cl| is listed in |users|"""
177 # See if we are listed in the approvals list. We have to parse
178 # this by hand as the gerrit query system doesn't support it :(
179 # http://code.google.com/p/gerrit/issues/detail?id=1235
180 if 'approvals' not in cl['currentPatchSet']:
181 return False
182
183 if isinstance(users, basestring):
184 users = (users,)
185
186 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700187 if (approver['by']['email'] in users and
188 approver['type'] == 'CRVW' and
189 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400190 return True
191
192 return False
193
194
195def UserActTodo(opts):
196 """List CLs needing your review"""
197 emails, reviewers, owners = _MyUserInfo()
198 cls = FilteredQuery(opts, '( %s ) status:open NOT ( %s )' %
199 (' OR '.join(reviewers), ' OR '.join(owners)))
200 cls = [x for x in cls if not IsApprover(x, emails)]
201 lims = limits(cls)
202 for cl in cls:
203 PrintCl(opts, cl, lims)
204
205
Mike Frysingera1db2c42014-06-15 00:42:48 -0700206def UserActSearch(opts, query):
207 """List CLs matching the Gerrit <search query>"""
208 cls = FilteredQuery(opts, query)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400209 lims = limits(cls)
210 for cl in cls:
211 PrintCl(opts, cl, lims)
212
213
Mike Frysingera1db2c42014-06-15 00:42:48 -0700214def UserActMine(opts):
215 """List your CLs with review statuses"""
216 _, _, owners = _MyUserInfo()
217 UserActSearch(opts, '( %s ) status:new' % (' OR '.join(owners),))
218
219
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700220def UserActInspect(opts, *args):
221 """Inspect CL number <n> [n ...]"""
222 for idx in args:
223 cl = FilteredQuery(opts, idx)
224 if cl:
225 PrintCl(opts, cl[0], None)
226 else:
227 print('no results found for CL %s' % idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400228
229
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700230def UserActReview(opts, *args):
231 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
232 num = args[-1]
233 for idx in args[:-1]:
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700234 opts.gerrit.SetReview(idx, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700235UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400236
237
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700238def UserActVerify(opts, *args):
239 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
240 num = args[-1]
241 for idx in args[:-1]:
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700242 opts.gerrit.SetReview(idx, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700243UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400244
245
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700246def UserActReady(opts, *args):
247 """Mark CL <n> [n ...] with ready status <0,1,2>"""
248 num = args[-1]
249 for idx in args[:-1]:
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700250 opts.gerrit.SetReview(idx, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700251UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400252
253
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700254def UserActSubmit(opts, *args):
255 """Submit CL <n> [n ...]"""
256 for idx in args:
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700257 opts.gerrit.SubmitChange(idx, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400258
259
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700260def UserActAbandon(opts, *args):
261 """Abandon CL <n> [n ...]"""
262 for idx in args:
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700263 opts.gerrit.AbandonChange(idx, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400264
265
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700266def UserActRestore(opts, *args):
267 """Restore CL <n> [n ...] that was abandoned"""
268 for idx in args:
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700269 opts.gerrit.RestoreChange(idx, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400270
271
Mike Frysingerc15efa52013-12-12 01:13:56 -0500272def UserActReviewers(opts, idx, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700273 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500274 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700275 # Allow for optional leading '~'.
276 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
277 add_list, remove_list, invalid_list = [], [], []
278
279 for x in emails:
280 if not email_validator.match(x):
281 invalid_list.append(x)
282 elif x[0] == '~':
283 remove_list.append(x[1:])
284 else:
285 add_list.append(x)
286
287 if invalid_list:
288 cros_build_lib.Die(
289 'Invalid email address(es): %s' % ', '.join(invalid_list))
290
291 if add_list or remove_list:
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700292 opts.gerrit.SetReviewers(idx, add=add_list, remove=remove_list,
293 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700294
295
Doug Anderson8119df02013-07-20 21:00:24 +0530296def UserActMessage(opts, idx, message):
297 """Add a message to CL <n>"""
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700298 opts.gerrit.SetReview(idx, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530299
300
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700301def UserActDeletedraft(opts, *args):
302 """Delete draft patch set <n> [n ...]"""
303 for idx in args:
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700304 opts.gerrit.DeleteDraft(idx, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800305
306
Mike Frysinger13f23a42013-05-13 17:32:01 -0400307def main(argv):
308 # Locate actions that are exposed to the user. All functions that start
309 # with "UserAct" are fair game.
310 act_pfx = 'UserAct'
311 actions = [x for x in globals() if x.startswith(act_pfx)]
312
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500313 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400314
315There is no support for doing line-by-line code review via the command line.
316This helps you manage various bits and CL status.
317
Mike Frysingera1db2c42014-06-15 00:42:48 -0700318For general Gerrit documentation, see:
319 https://gerrit-review.googlesource.com/Documentation/
320The Searching Changes page covers the search query syntax:
321 https://gerrit-review.googlesource.com/Documentation/user-search.html
322
Mike Frysinger13f23a42013-05-13 17:32:01 -0400323Example:
324 $ gerrit todo # List all the CLs that await your review.
325 $ gerrit mine # List all of your open CLs.
326 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
327 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
328 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700329Scripting:
330 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your CLs ready.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400331
332Actions:"""
333 indent = max([len(x) - len(act_pfx) for x in actions])
334 for a in sorted(actions):
335 usage += '\n %-*s: %s' % (indent, a[len(act_pfx):].lower(),
336 globals()[a].__doc__)
337
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500338 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500339 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Mike Frysinger40541c62014-02-08 04:38:37 -0500340 const=constants.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500341 help='Query internal Chromium Gerrit instance')
342 parser.add_argument('-g', '--gob',
343 help='Gerrit (on borg) instance to query (default: %s)' %
Mike Frysinger40541c62014-02-08 04:38:37 -0500344 (constants.EXTERNAL_GOB_INSTANCE))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500345 parser.add_argument('--sort', default='number',
346 help='Key to sort on (number, project)')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700347 parser.add_argument('--raw', default=False, action='store_true',
348 help='Return raw results (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700349 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
350 dest='dryrun',
351 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500352 parser.add_argument('-v', '--verbose', default=False, action='store_true',
353 help='Be more verbose in output')
354 parser.add_argument('args', nargs='+')
355 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400356
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400357 # pylint: disable=W0603
358 global COLOR
359 COLOR = terminal.Color(enabled=opts.color)
360
Mike Frysinger13f23a42013-05-13 17:32:01 -0400361 # TODO: This sucks. We assume that all actions which take an argument are
362 # a CL #. Or at least, there's no other reason for it to start with a *.
363 # We do this to automatically select internal vs external gerrit as this
364 # convention is encoded in much of our system. However, the rest of this
365 # script doesn't expect (or want) the leading *.
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500366 args = opts.args
Mike Frysinger13f23a42013-05-13 17:32:01 -0400367 if len(args) > 1:
368 if args[1][0] == '*':
Mike Frysinger40541c62014-02-08 04:38:37 -0500369 opts.gob = constants.INTERNAL_GOB_INSTANCE
Mike Frysinger13f23a42013-05-13 17:32:01 -0400370 args[1] = args[1][1:]
371
Mike Frysinger08737512014-02-07 22:58:26 -0500372 if opts.gob is None:
Mike Frysinger40541c62014-02-08 04:38:37 -0500373 opts.gob = constants.EXTERNAL_GOB_INSTANCE
Mike Frysinger08737512014-02-07 22:58:26 -0500374
Mike Frysinger40541c62014-02-08 04:38:37 -0500375 opts.gerrit = gerrit.GetGerritHelper(gob=opts.gob, print_cmd=opts.debug)
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500376 opts.Freeze()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400377
378 # Now look up the requested user action and run it.
379 cmd = args[0].lower()
380 args = args[1:]
381 functor = globals().get(act_pfx + cmd.capitalize())
382 if functor:
383 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700384 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700385 arg_min = getattr(functor, 'arg_min', len(argspec.args))
386 if len(args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700387 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700388 (cmd, arg_min))
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700389 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400390 parser.error('incorrect number of args: %s expects %s' %
391 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700392 try:
393 functor(opts, *args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500394 except (cros_build_lib.RunCommandError, gerrit.GerritException,
395 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700396 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400397 else:
398 parser.error('unknown action: %s' % (cmd,))