blob: 19bfbf5d25b35caa55a14381e705487769432246 [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
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070016import re
Mike Frysinger13f23a42013-05-13 17:32:01 -040017
Don Garrett88b8d782014-05-13 17:30:55 -070018from chromite.cbuildbot import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040019from chromite.lib import commandline
20from chromite.lib import cros_build_lib
21from chromite.lib import gerrit
Mike Frysingerc85d8162014-02-08 00:45:21 -050022from chromite.lib import gob_util
Mike Frysinger13f23a42013-05-13 17:32:01 -040023from chromite.lib import terminal
24
25
Mike Frysinger031ad0b2013-05-14 18:15:34 -040026COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040027
28# Map the internal names to the ones we normally show on the web ui.
29GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080030 'COMR': ['CQ', 'Commit Queue ',],
31 'CRVW': ['CR', 'Code Review ',],
32 'SUBM': ['S ', 'Submitted ',],
33 'TBVF': ['TV', 'Trybot Verified',],
34 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040035}
36
37# Order is important -- matches the web ui. This also controls the short
38# entries that we summarize in non-verbose mode.
39GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
40
41
42def red(s):
43 return COLOR.Color(terminal.Color.RED, s)
44
45
46def green(s):
47 return COLOR.Color(terminal.Color.GREEN, s)
48
49
50def blue(s):
51 return COLOR.Color(terminal.Color.BLUE, s)
52
53
54def limits(cls):
55 """Given a dict of fields, calculate the longest string lengths
56
57 This allows you to easily format the output of many results so that the
58 various cols all line up correctly.
59 """
60 lims = {}
61 for cl in cls:
62 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040063 # Use %s rather than str() to avoid codec issues.
64 # We also do this so we can format integers.
65 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040066 return lims
67
68
Mike Frysinger88f27292014-06-17 09:40:45 -070069# TODO: This func really needs to be merged into the core gerrit logic.
70def GetGerrit(opts, cl=None):
71 """Auto pick the right gerrit instance based on the |cl|
72
73 Args:
74 opts: The general options object.
75 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
76
77 Returns:
78 A tuple of a gerrit object and a sanitized CL #.
79 """
80 gob = opts.gob
81 if not cl is None:
82 if cl.startswith('*'):
83 gob = constants.INTERNAL_GOB_INSTANCE
84 cl = cl[1:]
85 elif ':' in cl:
86 gob, cl = cl.split(':', 1)
87
88 if not gob in opts.gerrit:
89 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
90
91 return (opts.gerrit[gob], cl)
92
93
Mike Frysinger13f23a42013-05-13 17:32:01 -040094def GetApprovalSummary(_opts, cls):
95 """Return a dict of the most important approvals"""
96 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
97 if 'approvals' in cls['currentPatchSet']:
98 for approver in cls['currentPatchSet']['approvals']:
99 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
100 if not cats:
101 cros_build_lib.Warning('unknown gerrit approval type: %s',
102 approver['type'])
103 continue
104 cat = cats[0].strip()
105 val = int(approver['value'])
106 if not cat in approvs:
107 # Ignore the extended categories in the summary view.
108 continue
109 elif approvs[cat] is '':
110 approvs[cat] = val
111 elif val < 0:
112 approvs[cat] = min(approvs[cat], val)
113 else:
114 approvs[cat] = max(approvs[cat], val)
115 return approvs
116
117
118def PrintCl(opts, cls, lims, show_approvals=True):
119 """Pretty print a single result"""
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700120 if opts.raw:
121 # Special case internal Chrome GoB as that is what most devs use.
122 # They can always redirect the list elsewhere via the -g option.
123 if opts.gob == constants.INTERNAL_GOB_INSTANCE:
124 print(constants.INTERNAL_CHANGE_PREFIX, end='')
125 print(cls['number'])
126 return
127
Mike Frysinger13f23a42013-05-13 17:32:01 -0400128 if not lims:
129 lims = {'url': 0, 'project': 0}
130
131 status = ''
132 if show_approvals and not opts.verbose:
133 approvs = GetApprovalSummary(opts, cls)
134 for cat in GERRIT_SUMMARY_CATS:
135 if approvs[cat] is '':
136 functor = lambda x: x
137 elif approvs[cat] < 0:
138 functor = red
139 else:
140 functor = green
141 status += functor('%s:%2s ' % (cat, approvs[cat]))
142
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500143 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cls['url'])), status,
144 lims['project'], cls['project'], cls['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400145
146 if show_approvals and opts.verbose:
147 for approver in cls['currentPatchSet'].get('approvals', []):
148 functor = red if int(approver['value']) < 0 else green
149 n = functor('%2s' % approver['value'])
150 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
151 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500152 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400153
154
155def _MyUserInfo():
156 username = os.environ['USER']
157 emails = ['%s@%s' % (username, domain)
158 for domain in ('google.com', 'chromium.org')]
159 reviewers = ['reviewer:%s' % x for x in emails]
160 owners = ['owner:%s' % x for x in emails]
161 return emails, reviewers, owners
162
163
164def FilteredQuery(opts, query):
165 """Query gerrit and filter/clean up the results"""
166 ret = []
167
Mike Frysinger88f27292014-06-17 09:40:45 -0700168 helper, _ = GetGerrit(opts)
169 for cl in helper.Query(query, raw=True, bypass_cache=False):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400170 # Gerrit likes to return a stats record too.
171 if not 'project' in cl:
172 continue
173
174 # Strip off common leading names since the result is still
175 # unique over the whole tree.
176 if not opts.verbose:
Mike Frysingere5e78272014-06-15 00:41:30 -0700177 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform',
178 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400179 if cl['project'].startswith('%s/' % pfx):
180 cl['project'] = cl['project'][len(pfx) + 1:]
181
182 ret.append(cl)
183
184 if opts.sort in ('number',):
185 key = lambda x: int(x[opts.sort])
186 else:
187 key = lambda x: x[opts.sort]
188 return sorted(ret, key=key)
189
190
Mike Frysinger13f23a42013-05-13 17:32:01 -0400191def IsApprover(cl, users):
192 """See if the approvers in |cl| is listed in |users|"""
193 # See if we are listed in the approvals list. We have to parse
194 # this by hand as the gerrit query system doesn't support it :(
195 # http://code.google.com/p/gerrit/issues/detail?id=1235
196 if 'approvals' not in cl['currentPatchSet']:
197 return False
198
199 if isinstance(users, basestring):
200 users = (users,)
201
202 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700203 if (approver['by']['email'] in users and
204 approver['type'] == 'CRVW' and
205 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400206 return True
207
208 return False
209
210
211def UserActTodo(opts):
212 """List CLs needing your review"""
213 emails, reviewers, owners = _MyUserInfo()
214 cls = FilteredQuery(opts, '( %s ) status:open NOT ( %s )' %
215 (' OR '.join(reviewers), ' OR '.join(owners)))
216 cls = [x for x in cls if not IsApprover(x, emails)]
217 lims = limits(cls)
218 for cl in cls:
219 PrintCl(opts, cl, lims)
220
221
Mike Frysingera1db2c42014-06-15 00:42:48 -0700222def UserActSearch(opts, query):
223 """List CLs matching the Gerrit <search query>"""
224 cls = FilteredQuery(opts, query)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400225 lims = limits(cls)
226 for cl in cls:
227 PrintCl(opts, cl, lims)
228
229
Mike Frysingera1db2c42014-06-15 00:42:48 -0700230def UserActMine(opts):
231 """List your CLs with review statuses"""
232 _, _, owners = _MyUserInfo()
233 UserActSearch(opts, '( %s ) status:new' % (' OR '.join(owners),))
234
235
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700236def UserActInspect(opts, *args):
237 """Inspect CL number <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700238 for arg in args:
239 cl = FilteredQuery(opts, arg)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700240 if cl:
241 PrintCl(opts, cl[0], None)
242 else:
Mike Frysinger88f27292014-06-17 09:40:45 -0700243 print('no results found for CL %s' % arg)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400244
245
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700246def UserActReview(opts, *args):
247 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
248 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700249 for arg in args[:-1]:
250 helper, cl = GetGerrit(opts, arg)
251 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700252UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400253
254
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700255def UserActVerify(opts, *args):
256 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
257 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700258 for arg in args[:-1]:
259 helper, cl = GetGerrit(opts, arg)
260 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700261UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400262
263
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700264def UserActReady(opts, *args):
265 """Mark CL <n> [n ...] with ready status <0,1,2>"""
266 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700267 for arg in args[:-1]:
268 helper, cl = GetGerrit(opts, arg)
269 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700270UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400271
272
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700273def UserActSubmit(opts, *args):
274 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700275 for arg in args:
276 helper, cl = GetGerrit(opts, arg)
277 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400278
279
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700280def UserActAbandon(opts, *args):
281 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700282 for arg in args:
283 helper, cl = GetGerrit(opts, arg)
284 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400285
286
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700287def UserActRestore(opts, *args):
288 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700289 for arg in args:
290 helper, cl = GetGerrit(opts, arg)
291 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400292
293
Mike Frysinger88f27292014-06-17 09:40:45 -0700294def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700295 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500296 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700297 # Allow for optional leading '~'.
298 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
299 add_list, remove_list, invalid_list = [], [], []
300
301 for x in emails:
302 if not email_validator.match(x):
303 invalid_list.append(x)
304 elif x[0] == '~':
305 remove_list.append(x[1:])
306 else:
307 add_list.append(x)
308
309 if invalid_list:
310 cros_build_lib.Die(
311 'Invalid email address(es): %s' % ', '.join(invalid_list))
312
313 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700314 helper, cl = GetGerrit(opts, cl)
315 helper.SetReviewers(cl, add=add_list, remove=remove_list,
316 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700317
318
Mike Frysinger88f27292014-06-17 09:40:45 -0700319def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530320 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700321 helper, cl = GetGerrit(opts, cl)
322 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530323
324
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700325def UserActDeletedraft(opts, *args):
326 """Delete draft patch set <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700327 for arg in args:
328 helper, cl = GetGerrit(opts, arg)
329 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800330
331
Mike Frysinger13f23a42013-05-13 17:32:01 -0400332def main(argv):
333 # Locate actions that are exposed to the user. All functions that start
334 # with "UserAct" are fair game.
335 act_pfx = 'UserAct'
336 actions = [x for x in globals() if x.startswith(act_pfx)]
337
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500338 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400339
340There is no support for doing line-by-line code review via the command line.
341This helps you manage various bits and CL status.
342
Mike Frysingera1db2c42014-06-15 00:42:48 -0700343For general Gerrit documentation, see:
344 https://gerrit-review.googlesource.com/Documentation/
345The Searching Changes page covers the search query syntax:
346 https://gerrit-review.googlesource.com/Documentation/user-search.html
347
Mike Frysinger13f23a42013-05-13 17:32:01 -0400348Example:
349 $ gerrit todo # List all the CLs that await your review.
350 $ gerrit mine # List all of your open CLs.
351 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
352 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
353 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700354Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700355 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
356ready.
357 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
358ready.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400359
360Actions:"""
361 indent = max([len(x) - len(act_pfx) for x in actions])
362 for a in sorted(actions):
363 usage += '\n %-*s: %s' % (indent, a[len(act_pfx):].lower(),
364 globals()[a].__doc__)
365
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500366 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500367 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Mike Frysinger88f27292014-06-17 09:40:45 -0700368 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysinger40541c62014-02-08 04:38:37 -0500369 const=constants.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500370 help='Query internal Chromium Gerrit instance')
371 parser.add_argument('-g', '--gob',
Mike Frysinger88f27292014-06-17 09:40:45 -0700372 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500373 help='Gerrit (on borg) instance to query (default: %s)' %
Mike Frysinger40541c62014-02-08 04:38:37 -0500374 (constants.EXTERNAL_GOB_INSTANCE))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500375 parser.add_argument('--sort', default='number',
376 help='Key to sort on (number, project)')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700377 parser.add_argument('--raw', default=False, action='store_true',
378 help='Return raw results (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700379 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
380 dest='dryrun',
381 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500382 parser.add_argument('-v', '--verbose', default=False, action='store_true',
383 help='Be more verbose in output')
384 parser.add_argument('args', nargs='+')
385 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400386
Mike Frysinger88f27292014-06-17 09:40:45 -0700387 # A cache of gerrit helpers we'll load on demand.
388 opts.gerrit = {}
389 opts.Freeze()
390
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400391 # pylint: disable=W0603
392 global COLOR
393 COLOR = terminal.Color(enabled=opts.color)
394
Mike Frysinger13f23a42013-05-13 17:32:01 -0400395 # Now look up the requested user action and run it.
Mike Frysinger88f27292014-06-17 09:40:45 -0700396 cmd = opts.args[0].lower()
397 args = opts.args[1:]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400398 functor = globals().get(act_pfx + cmd.capitalize())
399 if functor:
400 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700401 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700402 arg_min = getattr(functor, 'arg_min', len(argspec.args))
403 if len(args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700404 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700405 (cmd, arg_min))
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700406 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400407 parser.error('incorrect number of args: %s expects %s' %
408 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700409 try:
410 functor(opts, *args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500411 except (cros_build_lib.RunCommandError, gerrit.GerritException,
412 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700413 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400414 else:
415 parser.error('unknown action: %s' % (cmd,))