blob: 5a9bce9d2f558374afb8078990014f91257dd9d1 [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
23from 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
69def GetApprovalSummary(_opts, cls):
70 """Return a dict of the most important approvals"""
71 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
72 if 'approvals' in cls['currentPatchSet']:
73 for approver in cls['currentPatchSet']['approvals']:
74 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
75 if not cats:
76 cros_build_lib.Warning('unknown gerrit approval type: %s',
77 approver['type'])
78 continue
79 cat = cats[0].strip()
80 val = int(approver['value'])
81 if not cat in approvs:
82 # Ignore the extended categories in the summary view.
83 continue
84 elif approvs[cat] is '':
85 approvs[cat] = val
86 elif val < 0:
87 approvs[cat] = min(approvs[cat], val)
88 else:
89 approvs[cat] = max(approvs[cat], val)
90 return approvs
91
92
93def PrintCl(opts, cls, lims, show_approvals=True):
94 """Pretty print a single result"""
95 if not lims:
96 lims = {'url': 0, 'project': 0}
97
98 status = ''
99 if show_approvals and not opts.verbose:
100 approvs = GetApprovalSummary(opts, cls)
101 for cat in GERRIT_SUMMARY_CATS:
102 if approvs[cat] is '':
103 functor = lambda x: x
104 elif approvs[cat] < 0:
105 functor = red
106 else:
107 functor = green
108 status += functor('%s:%2s ' % (cat, approvs[cat]))
109
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500110 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cls['url'])), status,
111 lims['project'], cls['project'], cls['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400112
113 if show_approvals and opts.verbose:
114 for approver in cls['currentPatchSet'].get('approvals', []):
115 functor = red if int(approver['value']) < 0 else green
116 n = functor('%2s' % approver['value'])
117 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
118 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500119 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400120
121
122def _MyUserInfo():
123 username = os.environ['USER']
124 emails = ['%s@%s' % (username, domain)
125 for domain in ('google.com', 'chromium.org')]
126 reviewers = ['reviewer:%s' % x for x in emails]
127 owners = ['owner:%s' % x for x in emails]
128 return emails, reviewers, owners
129
130
131def FilteredQuery(opts, query):
132 """Query gerrit and filter/clean up the results"""
133 ret = []
134
135 for cl in opts.gerrit.Query(query, raw=True):
136 # Gerrit likes to return a stats record too.
137 if not 'project' in cl:
138 continue
139
140 # Strip off common leading names since the result is still
141 # unique over the whole tree.
142 if not opts.verbose:
143 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform'):
144 if cl['project'].startswith('%s/' % pfx):
145 cl['project'] = cl['project'][len(pfx) + 1:]
146
147 ret.append(cl)
148
149 if opts.sort in ('number',):
150 key = lambda x: int(x[opts.sort])
151 else:
152 key = lambda x: x[opts.sort]
153 return sorted(ret, key=key)
154
155
156def ChangeNumberToCommit(opts, idx):
157 """Given a gerrit CL #, return the revision info
158
159 This is the form that the gerrit ssh interface expects.
160 """
161 cl = opts.gerrit.QuerySingleRecord(idx, raw=True)
162 return cl['currentPatchSet']['revision']
163
164
Mike Frysinger13f23a42013-05-13 17:32:01 -0400165def IsApprover(cl, users):
166 """See if the approvers in |cl| is listed in |users|"""
167 # See if we are listed in the approvals list. We have to parse
168 # this by hand as the gerrit query system doesn't support it :(
169 # http://code.google.com/p/gerrit/issues/detail?id=1235
170 if 'approvals' not in cl['currentPatchSet']:
171 return False
172
173 if isinstance(users, basestring):
174 users = (users,)
175
176 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700177 if (approver['by']['email'] in users and
178 approver['type'] == 'CRVW' and
179 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400180 return True
181
182 return False
183
184
185def UserActTodo(opts):
186 """List CLs needing your review"""
187 emails, reviewers, owners = _MyUserInfo()
188 cls = FilteredQuery(opts, '( %s ) status:open NOT ( %s )' %
189 (' OR '.join(reviewers), ' OR '.join(owners)))
190 cls = [x for x in cls if not IsApprover(x, emails)]
191 lims = limits(cls)
192 for cl in cls:
193 PrintCl(opts, cl, lims)
194
195
196def UserActMine(opts):
197 """List your CLs with review statuses"""
198 _, _, owners = _MyUserInfo()
199 cls = FilteredQuery(opts, '( %s ) status:new' % (' OR '.join(owners),))
200 lims = limits(cls)
201 for cl in cls:
202 PrintCl(opts, cl, lims)
203
204
205def UserActInspect(opts, idx):
206 """Inspect CL number <n>"""
Mike Frysinger80d0b342014-02-08 04:33:36 -0500207 cl = FilteredQuery(opts, idx)
208 if cl:
209 PrintCl(opts, cl[0], None)
210 else:
211 print('no results found for CL %s' % idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400212
213
214def UserActReview(opts, idx, num):
215 """Mark CL <n> with code review status [-2,-1,0,1,2]"""
Stefan Zager29560302013-09-06 14:30:54 -0700216 opts.gerrit.SetReview(idx, labels={'Code-Review': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400217
218
219def UserActVerify(opts, idx, num):
220 """Mark CL <n> with verify status [-1,0,1]"""
Stefan Zager29560302013-09-06 14:30:54 -0700221 opts.gerrit.SetReview(idx, labels={'Verified': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400222
223
224def UserActReady(opts, idx, num):
225 """Mark CL <n> with ready status [-1,0,1]"""
Stefan Zager29560302013-09-06 14:30:54 -0700226 opts.gerrit.SetReview(idx, labels={'Commit-Queue': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400227
228
229def UserActSubmit(opts, idx):
230 """Submit CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700231 opts.gerrit.SubmitChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400232
233
234def UserActAbandon(opts, idx):
235 """Abandon CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700236 opts.gerrit.AbandonChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400237
238
239def UserActRestore(opts, idx):
240 """Restore CL <n> that was abandoned"""
Stefan Zager29560302013-09-06 14:30:54 -0700241 opts.gerrit.RestoreChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400242
243
Mike Frysingerc15efa52013-12-12 01:13:56 -0500244def UserActReviewers(opts, idx, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700245 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500246 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700247 # Allow for optional leading '~'.
248 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
249 add_list, remove_list, invalid_list = [], [], []
250
251 for x in emails:
252 if not email_validator.match(x):
253 invalid_list.append(x)
254 elif x[0] == '~':
255 remove_list.append(x[1:])
256 else:
257 add_list.append(x)
258
259 if invalid_list:
260 cros_build_lib.Die(
261 'Invalid email address(es): %s' % ', '.join(invalid_list))
262
263 if add_list or remove_list:
264 opts.gerrit.SetReviewers(idx, add=add_list, remove=remove_list)
265
266
Doug Anderson8119df02013-07-20 21:00:24 +0530267def UserActMessage(opts, idx, message):
268 """Add a message to CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700269 opts.gerrit.SetReview(idx, msg=message)
Doug Anderson8119df02013-07-20 21:00:24 +0530270
271
Mike Frysinger13f23a42013-05-13 17:32:01 -0400272def main(argv):
273 # Locate actions that are exposed to the user. All functions that start
274 # with "UserAct" are fair game.
275 act_pfx = 'UserAct'
276 actions = [x for x in globals() if x.startswith(act_pfx)]
277
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500278 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400279
280There is no support for doing line-by-line code review via the command line.
281This helps you manage various bits and CL status.
282
283Example:
284 $ gerrit todo # List all the CLs that await your review.
285 $ gerrit mine # List all of your open CLs.
286 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
287 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
288 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
289
290Actions:"""
291 indent = max([len(x) - len(act_pfx) for x in actions])
292 for a in sorted(actions):
293 usage += '\n %-*s: %s' % (indent, a[len(act_pfx):].lower(),
294 globals()[a].__doc__)
295
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500296 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500297 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
298 const=constants.INTERNAL_REMOTE,
299 help='Query internal Chromium Gerrit instance')
300 parser.add_argument('-g', '--gob',
301 help='Gerrit (on borg) instance to query (default: %s)' %
302 (constants.EXTERNAL_REMOTE))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500303 parser.add_argument('--sort', default='number',
304 help='Key to sort on (number, project)')
305 parser.add_argument('-v', '--verbose', default=False, action='store_true',
306 help='Be more verbose in output')
307 parser.add_argument('args', nargs='+')
308 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400309
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400310 # pylint: disable=W0603
311 global COLOR
312 COLOR = terminal.Color(enabled=opts.color)
313
Mike Frysinger13f23a42013-05-13 17:32:01 -0400314 # TODO: This sucks. We assume that all actions which take an argument are
315 # a CL #. Or at least, there's no other reason for it to start with a *.
316 # We do this to automatically select internal vs external gerrit as this
317 # convention is encoded in much of our system. However, the rest of this
318 # script doesn't expect (or want) the leading *.
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500319 args = opts.args
Mike Frysinger13f23a42013-05-13 17:32:01 -0400320 if len(args) > 1:
321 if args[1][0] == '*':
Mike Frysinger08737512014-02-07 22:58:26 -0500322 opts.gob = constants.INTERNAL_REMOTE
Mike Frysinger13f23a42013-05-13 17:32:01 -0400323 args[1] = args[1][1:]
324
Mike Frysinger08737512014-02-07 22:58:26 -0500325 if opts.gob is None:
326 opts.gob = constants.EXTERNAL_REMOTE
327
328 opts.gerrit = gerrit.GetGerritHelper(opts.gob, print_cmd=opts.debug)
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500329 opts.Freeze()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400330
331 # Now look up the requested user action and run it.
332 cmd = args[0].lower()
333 args = args[1:]
334 functor = globals().get(act_pfx + cmd.capitalize())
335 if functor:
336 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700337 if argspec.varargs:
338 if len(args) < len(argspec.args):
339 parser.error('incorrect number of args: %s expects at least %s' %
340 (cmd, len(argspec.args)))
341 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400342 parser.error('incorrect number of args: %s expects %s' %
343 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700344 try:
345 functor(opts, *args)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700346 except (cros_build_lib.RunCommandError, gerrit.GerritException) as e:
347 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400348 else:
349 parser.error('unknown action: %s' % (cmd,))