blob: 70829ab19a97bdc94f8b125aa8292ae5e153e7f8 [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
19from chromite.buildbot import constants
20from 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
136 for cl in opts.gerrit.Query(query, raw=True):
137 # 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:
144 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform'):
145 if cl['project'].startswith('%s/' % pfx):
146 cl['project'] = cl['project'][len(pfx) + 1:]
147
148 ret.append(cl)
149
150 if opts.sort in ('number',):
151 key = lambda x: int(x[opts.sort])
152 else:
153 key = lambda x: x[opts.sort]
154 return sorted(ret, key=key)
155
156
157def ChangeNumberToCommit(opts, idx):
158 """Given a gerrit CL #, return the revision info
159
160 This is the form that the gerrit ssh interface expects.
161 """
162 cl = opts.gerrit.QuerySingleRecord(idx, raw=True)
163 return cl['currentPatchSet']['revision']
164
165
Mike Frysinger13f23a42013-05-13 17:32:01 -0400166def IsApprover(cl, users):
167 """See if the approvers in |cl| is listed in |users|"""
168 # See if we are listed in the approvals list. We have to parse
169 # this by hand as the gerrit query system doesn't support it :(
170 # http://code.google.com/p/gerrit/issues/detail?id=1235
171 if 'approvals' not in cl['currentPatchSet']:
172 return False
173
174 if isinstance(users, basestring):
175 users = (users,)
176
177 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700178 if (approver['by']['email'] in users and
179 approver['type'] == 'CRVW' and
180 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400181 return True
182
183 return False
184
185
186def UserActTodo(opts):
187 """List CLs needing your review"""
188 emails, reviewers, owners = _MyUserInfo()
189 cls = FilteredQuery(opts, '( %s ) status:open NOT ( %s )' %
190 (' OR '.join(reviewers), ' OR '.join(owners)))
191 cls = [x for x in cls if not IsApprover(x, emails)]
192 lims = limits(cls)
193 for cl in cls:
194 PrintCl(opts, cl, lims)
195
196
197def UserActMine(opts):
198 """List your CLs with review statuses"""
199 _, _, owners = _MyUserInfo()
200 cls = FilteredQuery(opts, '( %s ) status:new' % (' OR '.join(owners),))
201 lims = limits(cls)
202 for cl in cls:
203 PrintCl(opts, cl, lims)
204
205
206def UserActInspect(opts, idx):
207 """Inspect CL number <n>"""
Mike Frysinger80d0b342014-02-08 04:33:36 -0500208 cl = FilteredQuery(opts, idx)
209 if cl:
210 PrintCl(opts, cl[0], None)
211 else:
212 print('no results found for CL %s' % idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400213
214
215def UserActReview(opts, idx, num):
216 """Mark CL <n> with code review status [-2,-1,0,1,2]"""
Stefan Zager29560302013-09-06 14:30:54 -0700217 opts.gerrit.SetReview(idx, labels={'Code-Review': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400218
219
220def UserActVerify(opts, idx, num):
221 """Mark CL <n> with verify status [-1,0,1]"""
Stefan Zager29560302013-09-06 14:30:54 -0700222 opts.gerrit.SetReview(idx, labels={'Verified': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400223
224
225def UserActReady(opts, idx, num):
226 """Mark CL <n> with ready status [-1,0,1]"""
Stefan Zager29560302013-09-06 14:30:54 -0700227 opts.gerrit.SetReview(idx, labels={'Commit-Queue': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400228
229
230def UserActSubmit(opts, idx):
231 """Submit CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700232 opts.gerrit.SubmitChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400233
234
235def UserActAbandon(opts, idx):
236 """Abandon CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700237 opts.gerrit.AbandonChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400238
239
240def UserActRestore(opts, idx):
241 """Restore CL <n> that was abandoned"""
Stefan Zager29560302013-09-06 14:30:54 -0700242 opts.gerrit.RestoreChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400243
244
Mike Frysingerc15efa52013-12-12 01:13:56 -0500245def UserActReviewers(opts, idx, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700246 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500247 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700248 # Allow for optional leading '~'.
249 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
250 add_list, remove_list, invalid_list = [], [], []
251
252 for x in emails:
253 if not email_validator.match(x):
254 invalid_list.append(x)
255 elif x[0] == '~':
256 remove_list.append(x[1:])
257 else:
258 add_list.append(x)
259
260 if invalid_list:
261 cros_build_lib.Die(
262 'Invalid email address(es): %s' % ', '.join(invalid_list))
263
264 if add_list or remove_list:
265 opts.gerrit.SetReviewers(idx, add=add_list, remove=remove_list)
266
267
Doug Anderson8119df02013-07-20 21:00:24 +0530268def UserActMessage(opts, idx, message):
269 """Add a message to CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700270 opts.gerrit.SetReview(idx, msg=message)
Doug Anderson8119df02013-07-20 21:00:24 +0530271
272
Mike Frysinger13f23a42013-05-13 17:32:01 -0400273def main(argv):
274 # Locate actions that are exposed to the user. All functions that start
275 # with "UserAct" are fair game.
276 act_pfx = 'UserAct'
277 actions = [x for x in globals() if x.startswith(act_pfx)]
278
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500279 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400280
281There is no support for doing line-by-line code review via the command line.
282This helps you manage various bits and CL status.
283
284Example:
285 $ gerrit todo # List all the CLs that await your review.
286 $ gerrit mine # List all of your open CLs.
287 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
288 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
289 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
290
291Actions:"""
292 indent = max([len(x) - len(act_pfx) for x in actions])
293 for a in sorted(actions):
294 usage += '\n %-*s: %s' % (indent, a[len(act_pfx):].lower(),
295 globals()[a].__doc__)
296
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500297 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500298 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
299 const=constants.INTERNAL_REMOTE,
300 help='Query internal Chromium Gerrit instance')
301 parser.add_argument('-g', '--gob',
302 help='Gerrit (on borg) instance to query (default: %s)' %
303 (constants.EXTERNAL_REMOTE))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500304 parser.add_argument('--sort', default='number',
305 help='Key to sort on (number, project)')
306 parser.add_argument('-v', '--verbose', default=False, action='store_true',
307 help='Be more verbose in output')
308 parser.add_argument('args', nargs='+')
309 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400310
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400311 # pylint: disable=W0603
312 global COLOR
313 COLOR = terminal.Color(enabled=opts.color)
314
Mike Frysinger13f23a42013-05-13 17:32:01 -0400315 # TODO: This sucks. We assume that all actions which take an argument are
316 # a CL #. Or at least, there's no other reason for it to start with a *.
317 # We do this to automatically select internal vs external gerrit as this
318 # convention is encoded in much of our system. However, the rest of this
319 # script doesn't expect (or want) the leading *.
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500320 args = opts.args
Mike Frysinger13f23a42013-05-13 17:32:01 -0400321 if len(args) > 1:
322 if args[1][0] == '*':
Mike Frysinger08737512014-02-07 22:58:26 -0500323 opts.gob = constants.INTERNAL_REMOTE
Mike Frysinger13f23a42013-05-13 17:32:01 -0400324 args[1] = args[1][1:]
325
Mike Frysinger08737512014-02-07 22:58:26 -0500326 if opts.gob is None:
327 opts.gob = constants.EXTERNAL_REMOTE
328
329 opts.gerrit = gerrit.GetGerritHelper(opts.gob, print_cmd=opts.debug)
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500330 opts.Freeze()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400331
332 # Now look up the requested user action and run it.
333 cmd = args[0].lower()
334 args = args[1:]
335 functor = globals().get(act_pfx + cmd.capitalize())
336 if functor:
337 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700338 if argspec.varargs:
339 if len(args) < len(argspec.args):
340 parser.error('incorrect number of args: %s expects at least %s' %
341 (cmd, len(argspec.args)))
342 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400343 parser.error('incorrect number of args: %s expects %s' %
344 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700345 try:
346 functor(opts, *args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500347 except (cros_build_lib.RunCommandError, gerrit.GerritException,
348 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700349 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400350 else:
351 parser.error('unknown action: %s' % (cmd,))