blob: f1555a1b8cde4279184c552e05d364bfde9a7242 [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
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -080015import pprint
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
Mathieu Olivari04b4d522014-12-18 17:26:34 -080022from chromite.lib import git
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():
Mathieu Olivari04b4d522014-12-18 17:26:34 -0800157 email = git.GetProjectUserEmail(constants.CHROMITE_DIR)
158 [username, _, domain] = email.partition('@')
159 if domain in ('google.com', 'chromium.org'):
160 emails = ['%s@%s' % (username, domain)
161 for domain in ('google.com', 'chromium.org')]
162 else:
163 emails = [email]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400164 reviewers = ['reviewer:%s' % x for x in emails]
165 owners = ['owner:%s' % x for x in emails]
166 return emails, reviewers, owners
167
168
169def FilteredQuery(opts, query):
170 """Query gerrit and filter/clean up the results"""
171 ret = []
172
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800173 if opts.branch is not None:
174 query += ' branch:%s' % opts.branch
175
Mike Frysinger88f27292014-06-17 09:40:45 -0700176 helper, _ = GetGerrit(opts)
177 for cl in helper.Query(query, raw=True, bypass_cache=False):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400178 # Gerrit likes to return a stats record too.
179 if not 'project' in cl:
180 continue
181
182 # Strip off common leading names since the result is still
183 # unique over the whole tree.
184 if not opts.verbose:
Mike Frysingere5e78272014-06-15 00:41:30 -0700185 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform',
186 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400187 if cl['project'].startswith('%s/' % pfx):
188 cl['project'] = cl['project'][len(pfx) + 1:]
189
190 ret.append(cl)
191
192 if opts.sort in ('number',):
193 key = lambda x: int(x[opts.sort])
194 else:
195 key = lambda x: x[opts.sort]
196 return sorted(ret, key=key)
197
198
Mike Frysinger13f23a42013-05-13 17:32:01 -0400199def IsApprover(cl, users):
200 """See if the approvers in |cl| is listed in |users|"""
201 # See if we are listed in the approvals list. We have to parse
202 # this by hand as the gerrit query system doesn't support it :(
203 # http://code.google.com/p/gerrit/issues/detail?id=1235
204 if 'approvals' not in cl['currentPatchSet']:
205 return False
206
207 if isinstance(users, basestring):
208 users = (users,)
209
210 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700211 if (approver['by']['email'] in users and
212 approver['type'] == 'CRVW' and
213 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400214 return True
215
216 return False
217
218
219def UserActTodo(opts):
220 """List CLs needing your review"""
221 emails, reviewers, owners = _MyUserInfo()
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500222 cls = FilteredQuery(opts, ('( %s ) status:open NOT ( %s )' %
223 (' OR '.join(reviewers), ' OR '.join(owners))))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400224 cls = [x for x in cls if not IsApprover(x, emails)]
225 lims = limits(cls)
226 for cl in cls:
227 PrintCl(opts, cl, lims)
228
229
Mike Frysingera1db2c42014-06-15 00:42:48 -0700230def UserActSearch(opts, query):
231 """List CLs matching the Gerrit <search query>"""
232 cls = FilteredQuery(opts, query)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400233 lims = limits(cls)
234 for cl in cls:
235 PrintCl(opts, cl, lims)
236
237
Mike Frysingera1db2c42014-06-15 00:42:48 -0700238def UserActMine(opts):
239 """List your CLs with review statuses"""
240 _, _, owners = _MyUserInfo()
241 UserActSearch(opts, '( %s ) status:new' % (' OR '.join(owners),))
242
243
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700244def UserActInspect(opts, *args):
245 """Inspect CL number <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700246 for arg in args:
247 cl = FilteredQuery(opts, arg)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700248 if cl:
249 PrintCl(opts, cl[0], None)
250 else:
Mike Frysinger88f27292014-06-17 09:40:45 -0700251 print('no results found for CL %s' % arg)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400252
253
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700254def UserActReview(opts, *args):
255 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
256 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700257 for arg in args[:-1]:
258 helper, cl = GetGerrit(opts, arg)
259 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700260UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400261
262
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700263def UserActVerify(opts, *args):
264 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
265 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700266 for arg in args[:-1]:
267 helper, cl = GetGerrit(opts, arg)
268 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700269UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400270
271
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700272def UserActReady(opts, *args):
273 """Mark CL <n> [n ...] with ready status <0,1,2>"""
274 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700275 for arg in args[:-1]:
276 helper, cl = GetGerrit(opts, arg)
277 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700278UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400279
280
Mike Frysinger15b23e42014-12-05 17:00:05 -0500281def UserActTrybotready(opts, *args):
282 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
283 num = args[-1]
284 for arg in args[:-1]:
285 helper, cl = GetGerrit(opts, arg)
286 helper.SetReview(cl, labels={'Trybot-Ready': num}, dryrun=opts.dryrun)
287UserActTrybotready.arg_min = 2
288
289
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700290def UserActSubmit(opts, *args):
291 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700292 for arg in args:
293 helper, cl = GetGerrit(opts, arg)
294 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400295
296
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700297def UserActAbandon(opts, *args):
298 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700299 for arg in args:
300 helper, cl = GetGerrit(opts, arg)
301 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400302
303
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700304def UserActRestore(opts, *args):
305 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700306 for arg in args:
307 helper, cl = GetGerrit(opts, arg)
308 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400309
310
Mike Frysinger88f27292014-06-17 09:40:45 -0700311def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700312 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500313 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700314 # Allow for optional leading '~'.
315 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
316 add_list, remove_list, invalid_list = [], [], []
317
318 for x in emails:
319 if not email_validator.match(x):
320 invalid_list.append(x)
321 elif x[0] == '~':
322 remove_list.append(x[1:])
323 else:
324 add_list.append(x)
325
326 if invalid_list:
327 cros_build_lib.Die(
328 'Invalid email address(es): %s' % ', '.join(invalid_list))
329
330 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700331 helper, cl = GetGerrit(opts, cl)
332 helper.SetReviewers(cl, add=add_list, remove=remove_list,
333 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700334
335
Mike Frysinger88f27292014-06-17 09:40:45 -0700336def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530337 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700338 helper, cl = GetGerrit(opts, cl)
339 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530340
341
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700342def UserActDeletedraft(opts, *args):
343 """Delete draft patch set <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700344 for arg in args:
345 helper, cl = GetGerrit(opts, arg)
346 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800347
348
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800349def UserActAccount(opts):
350 """Get user account information."""
351 helper, _ = GetGerrit(opts)
352 pprint.PrettyPrinter().pprint(helper.GetAccount())
353
354
Mike Frysinger13f23a42013-05-13 17:32:01 -0400355def main(argv):
356 # Locate actions that are exposed to the user. All functions that start
357 # with "UserAct" are fair game.
358 act_pfx = 'UserAct'
359 actions = [x for x in globals() if x.startswith(act_pfx)]
360
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500361 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400362
363There is no support for doing line-by-line code review via the command line.
364This helps you manage various bits and CL status.
365
Mike Frysingera1db2c42014-06-15 00:42:48 -0700366For general Gerrit documentation, see:
367 https://gerrit-review.googlesource.com/Documentation/
368The Searching Changes page covers the search query syntax:
369 https://gerrit-review.googlesource.com/Documentation/user-search.html
370
Mike Frysinger13f23a42013-05-13 17:32:01 -0400371Example:
372 $ gerrit todo # List all the CLs that await your review.
373 $ gerrit mine # List all of your open CLs.
374 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
375 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
376 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700377Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700378 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
379ready.
380 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
381ready.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400382
383Actions:"""
384 indent = max([len(x) - len(act_pfx) for x in actions])
385 for a in sorted(actions):
Mike Frysinger15b23e42014-12-05 17:00:05 -0500386 cmd = a[len(act_pfx):]
387 # Sanity check for devs adding new commands. Should be quick.
388 if cmd != cmd.lower().capitalize():
389 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
390 (cmd, cmd.lower().capitalize()))
391 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400392
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500393 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500394 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Mike Frysinger88f27292014-06-17 09:40:45 -0700395 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysinger40541c62014-02-08 04:38:37 -0500396 const=constants.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500397 help='Query internal Chromium Gerrit instance')
398 parser.add_argument('-g', '--gob',
Mike Frysinger88f27292014-06-17 09:40:45 -0700399 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500400 help=('Gerrit (on borg) instance to query (default: %s)' %
401 (constants.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500402 parser.add_argument('--sort', default='number',
403 help='Key to sort on (number, project)')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700404 parser.add_argument('--raw', default=False, action='store_true',
405 help='Return raw results (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700406 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
407 dest='dryrun',
408 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500409 parser.add_argument('-v', '--verbose', default=False, action='store_true',
410 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800411 parser.add_argument('-b', '--branch',
412 help='Limit output to the specific branch')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500413 parser.add_argument('args', nargs='+')
414 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400415
Mike Frysinger88f27292014-06-17 09:40:45 -0700416 # A cache of gerrit helpers we'll load on demand.
417 opts.gerrit = {}
418 opts.Freeze()
419
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400420 # pylint: disable=W0603
421 global COLOR
422 COLOR = terminal.Color(enabled=opts.color)
423
Mike Frysinger13f23a42013-05-13 17:32:01 -0400424 # Now look up the requested user action and run it.
Mike Frysinger88f27292014-06-17 09:40:45 -0700425 cmd = opts.args[0].lower()
426 args = opts.args[1:]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400427 functor = globals().get(act_pfx + cmd.capitalize())
428 if functor:
429 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700430 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700431 arg_min = getattr(functor, 'arg_min', len(argspec.args))
432 if len(args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700433 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700434 (cmd, arg_min))
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700435 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400436 parser.error('incorrect number of args: %s expects %s' %
437 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700438 try:
439 functor(opts, *args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500440 except (cros_build_lib.RunCommandError, gerrit.GerritException,
441 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700442 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400443 else:
444 parser.error('unknown action: %s' % (cmd,))