blob: 1581d82b315ea181775a2cd10b1fd2359599f1d1 [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"""
96 if not lims:
97 lims = {'url': 0, 'project': 0}
98
99 status = ''
100 if show_approvals and not opts.verbose:
101 approvs = GetApprovalSummary(opts, cls)
102 for cat in GERRIT_SUMMARY_CATS:
103 if approvs[cat] is '':
104 functor = lambda x: x
105 elif approvs[cat] < 0:
106 functor = red
107 else:
108 functor = green
109 status += functor('%s:%2s ' % (cat, approvs[cat]))
110
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500111 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cls['url'])), status,
112 lims['project'], cls['project'], cls['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400113
114 if show_approvals and opts.verbose:
115 for approver in cls['currentPatchSet'].get('approvals', []):
116 functor = red if int(approver['value']) < 0 else green
117 n = functor('%2s' % approver['value'])
118 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
119 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500120 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400121
122
123def _MyUserInfo():
124 username = os.environ['USER']
125 emails = ['%s@%s' % (username, domain)
126 for domain in ('google.com', 'chromium.org')]
127 reviewers = ['reviewer:%s' % x for x in emails]
128 owners = ['owner:%s' % x for x in emails]
129 return emails, reviewers, owners
130
131
132def FilteredQuery(opts, query):
133 """Query gerrit and filter/clean up the results"""
134 ret = []
135
Mike Frysinger20fac842014-06-15 00:40:00 -0700136 for cl in opts.gerrit.Query(query, raw=True, bypass_cache=False):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400137 # Gerrit likes to return a stats record too.
138 if not 'project' in cl:
139 continue
140
141 # Strip off common leading names since the result is still
142 # unique over the whole tree.
143 if not opts.verbose:
Mike Frysingere5e78272014-06-15 00:41:30 -0700144 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform',
145 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400146 if cl['project'].startswith('%s/' % pfx):
147 cl['project'] = cl['project'][len(pfx) + 1:]
148
149 ret.append(cl)
150
151 if opts.sort in ('number',):
152 key = lambda x: int(x[opts.sort])
153 else:
154 key = lambda x: x[opts.sort]
155 return sorted(ret, key=key)
156
157
158def ChangeNumberToCommit(opts, idx):
159 """Given a gerrit CL #, return the revision info
160
161 This is the form that the gerrit ssh interface expects.
162 """
163 cl = opts.gerrit.QuerySingleRecord(idx, raw=True)
164 return cl['currentPatchSet']['revision']
165
166
Mike Frysinger13f23a42013-05-13 17:32:01 -0400167def IsApprover(cl, users):
168 """See if the approvers in |cl| is listed in |users|"""
169 # See if we are listed in the approvals list. We have to parse
170 # this by hand as the gerrit query system doesn't support it :(
171 # http://code.google.com/p/gerrit/issues/detail?id=1235
172 if 'approvals' not in cl['currentPatchSet']:
173 return False
174
175 if isinstance(users, basestring):
176 users = (users,)
177
178 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700179 if (approver['by']['email'] in users and
180 approver['type'] == 'CRVW' and
181 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400182 return True
183
184 return False
185
186
187def UserActTodo(opts):
188 """List CLs needing your review"""
189 emails, reviewers, owners = _MyUserInfo()
190 cls = FilteredQuery(opts, '( %s ) status:open NOT ( %s )' %
191 (' OR '.join(reviewers), ' OR '.join(owners)))
192 cls = [x for x in cls if not IsApprover(x, emails)]
193 lims = limits(cls)
194 for cl in cls:
195 PrintCl(opts, cl, lims)
196
197
Mike Frysingera1db2c42014-06-15 00:42:48 -0700198def UserActSearch(opts, query):
199 """List CLs matching the Gerrit <search query>"""
200 cls = FilteredQuery(opts, query)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400201 lims = limits(cls)
202 for cl in cls:
203 PrintCl(opts, cl, lims)
204
205
Mike Frysingera1db2c42014-06-15 00:42:48 -0700206def UserActMine(opts):
207 """List your CLs with review statuses"""
208 _, _, owners = _MyUserInfo()
209 UserActSearch(opts, '( %s ) status:new' % (' OR '.join(owners),))
210
211
Mike Frysinger13f23a42013-05-13 17:32:01 -0400212def UserActInspect(opts, idx):
213 """Inspect CL number <n>"""
Mike Frysinger80d0b342014-02-08 04:33:36 -0500214 cl = FilteredQuery(opts, idx)
215 if cl:
216 PrintCl(opts, cl[0], None)
217 else:
218 print('no results found for CL %s' % idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400219
220
221def UserActReview(opts, idx, num):
222 """Mark CL <n> with code review status [-2,-1,0,1,2]"""
Stefan Zager29560302013-09-06 14:30:54 -0700223 opts.gerrit.SetReview(idx, labels={'Code-Review': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400224
225
226def UserActVerify(opts, idx, num):
227 """Mark CL <n> with verify status [-1,0,1]"""
Stefan Zager29560302013-09-06 14:30:54 -0700228 opts.gerrit.SetReview(idx, labels={'Verified': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400229
230
231def UserActReady(opts, idx, num):
Aviv Keshet33806cb2014-06-11 09:20:32 -0700232 """Mark CL <n> with ready status [0,1,2]"""
Stefan Zager29560302013-09-06 14:30:54 -0700233 opts.gerrit.SetReview(idx, labels={'Commit-Queue': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400234
235
236def UserActSubmit(opts, idx):
237 """Submit CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700238 opts.gerrit.SubmitChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400239
240
241def UserActAbandon(opts, idx):
242 """Abandon CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700243 opts.gerrit.AbandonChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400244
245
246def UserActRestore(opts, idx):
247 """Restore CL <n> that was abandoned"""
Stefan Zager29560302013-09-06 14:30:54 -0700248 opts.gerrit.RestoreChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400249
250
Mike Frysingerc15efa52013-12-12 01:13:56 -0500251def UserActReviewers(opts, idx, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700252 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500253 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700254 # Allow for optional leading '~'.
255 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
256 add_list, remove_list, invalid_list = [], [], []
257
258 for x in emails:
259 if not email_validator.match(x):
260 invalid_list.append(x)
261 elif x[0] == '~':
262 remove_list.append(x[1:])
263 else:
264 add_list.append(x)
265
266 if invalid_list:
267 cros_build_lib.Die(
268 'Invalid email address(es): %s' % ', '.join(invalid_list))
269
270 if add_list or remove_list:
271 opts.gerrit.SetReviewers(idx, add=add_list, remove=remove_list)
272
273
Doug Anderson8119df02013-07-20 21:00:24 +0530274def UserActMessage(opts, idx, message):
275 """Add a message to CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700276 opts.gerrit.SetReview(idx, msg=message)
Doug Anderson8119df02013-07-20 21:00:24 +0530277
278
Jon Salza427fb02014-03-07 18:13:17 +0800279def UserActDeletedraft(opts, idx):
280 """Delete draft patch set <n>"""
281 opts.gerrit.DeleteDraft(idx)
282
283
Mike Frysinger13f23a42013-05-13 17:32:01 -0400284def main(argv):
285 # Locate actions that are exposed to the user. All functions that start
286 # with "UserAct" are fair game.
287 act_pfx = 'UserAct'
288 actions = [x for x in globals() if x.startswith(act_pfx)]
289
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500290 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400291
292There is no support for doing line-by-line code review via the command line.
293This helps you manage various bits and CL status.
294
Mike Frysingera1db2c42014-06-15 00:42:48 -0700295For general Gerrit documentation, see:
296 https://gerrit-review.googlesource.com/Documentation/
297The Searching Changes page covers the search query syntax:
298 https://gerrit-review.googlesource.com/Documentation/user-search.html
299
Mike Frysinger13f23a42013-05-13 17:32:01 -0400300Example:
301 $ gerrit todo # List all the CLs that await your review.
302 $ gerrit mine # List all of your open CLs.
303 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
304 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
305 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
306
307Actions:"""
308 indent = max([len(x) - len(act_pfx) for x in actions])
309 for a in sorted(actions):
310 usage += '\n %-*s: %s' % (indent, a[len(act_pfx):].lower(),
311 globals()[a].__doc__)
312
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500313 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500314 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Mike Frysinger40541c62014-02-08 04:38:37 -0500315 const=constants.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500316 help='Query internal Chromium Gerrit instance')
317 parser.add_argument('-g', '--gob',
318 help='Gerrit (on borg) instance to query (default: %s)' %
Mike Frysinger40541c62014-02-08 04:38:37 -0500319 (constants.EXTERNAL_GOB_INSTANCE))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500320 parser.add_argument('--sort', default='number',
321 help='Key to sort on (number, project)')
322 parser.add_argument('-v', '--verbose', default=False, action='store_true',
323 help='Be more verbose in output')
324 parser.add_argument('args', nargs='+')
325 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400326
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400327 # pylint: disable=W0603
328 global COLOR
329 COLOR = terminal.Color(enabled=opts.color)
330
Mike Frysinger13f23a42013-05-13 17:32:01 -0400331 # TODO: This sucks. We assume that all actions which take an argument are
332 # a CL #. Or at least, there's no other reason for it to start with a *.
333 # We do this to automatically select internal vs external gerrit as this
334 # convention is encoded in much of our system. However, the rest of this
335 # script doesn't expect (or want) the leading *.
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500336 args = opts.args
Mike Frysinger13f23a42013-05-13 17:32:01 -0400337 if len(args) > 1:
338 if args[1][0] == '*':
Mike Frysinger40541c62014-02-08 04:38:37 -0500339 opts.gob = constants.INTERNAL_GOB_INSTANCE
Mike Frysinger13f23a42013-05-13 17:32:01 -0400340 args[1] = args[1][1:]
341
Mike Frysinger08737512014-02-07 22:58:26 -0500342 if opts.gob is None:
Mike Frysinger40541c62014-02-08 04:38:37 -0500343 opts.gob = constants.EXTERNAL_GOB_INSTANCE
Mike Frysinger08737512014-02-07 22:58:26 -0500344
Mike Frysinger40541c62014-02-08 04:38:37 -0500345 opts.gerrit = gerrit.GetGerritHelper(gob=opts.gob, print_cmd=opts.debug)
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500346 opts.Freeze()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400347
348 # Now look up the requested user action and run it.
349 cmd = args[0].lower()
350 args = args[1:]
351 functor = globals().get(act_pfx + cmd.capitalize())
352 if functor:
353 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700354 if argspec.varargs:
355 if len(args) < len(argspec.args):
356 parser.error('incorrect number of args: %s expects at least %s' %
357 (cmd, len(argspec.args)))
358 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400359 parser.error('incorrect number of args: %s expects %s' %
360 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700361 try:
362 functor(opts, *args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500363 except (cros_build_lib.RunCommandError, gerrit.GerritException,
364 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700365 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400366 else:
367 parser.error('unknown action: %s' % (cmd,))