blob: 0ab418361a645e500644e22d392b46ef338fd30f [file] [log] [blame]
Mike Frysinger13f23a42013-05-13 17:32:01 -04001#!/usr/bin/python
2
3# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""A command line interface to the ChromeOS gerrit instances
8
9Internal Note:
10To expose a function directly to the command line interface, name your function
11with the prefix "UserAct".
12"""
13
14import inspect
15import os
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070016import re
Mike Frysinger13f23a42013-05-13 17:32:01 -040017
18from chromite.buildbot import constants
19from chromite.lib import commandline
20from chromite.lib import cros_build_lib
21from chromite.lib import gerrit
22from chromite.lib import terminal
23
24
Mike Frysinger031ad0b2013-05-14 18:15:34 -040025COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040026
27# Map the internal names to the ones we normally show on the web ui.
28GERRIT_APPROVAL_MAP = {
29 'COMR': ['CQ', 'Commit Queue',],
30 'CRVW': ['CR', 'Code Review ',],
31 'SUBM': ['S ', 'Submitted ',],
32 'VRIF': ['V ', 'Verified ',],
33}
34
35# Order is important -- matches the web ui. This also controls the short
36# entries that we summarize in non-verbose mode.
37GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
38
39
40def red(s):
41 return COLOR.Color(terminal.Color.RED, s)
42
43
44def green(s):
45 return COLOR.Color(terminal.Color.GREEN, s)
46
47
48def blue(s):
49 return COLOR.Color(terminal.Color.BLUE, s)
50
51
52def limits(cls):
53 """Given a dict of fields, calculate the longest string lengths
54
55 This allows you to easily format the output of many results so that the
56 various cols all line up correctly.
57 """
58 lims = {}
59 for cl in cls:
60 for k in cl.keys():
61 lims[k] = max(lims.get(k, 0), len(str(cl[k])))
62 return lims
63
64
65def GetApprovalSummary(_opts, cls):
66 """Return a dict of the most important approvals"""
67 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
68 if 'approvals' in cls['currentPatchSet']:
69 for approver in cls['currentPatchSet']['approvals']:
70 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
71 if not cats:
72 cros_build_lib.Warning('unknown gerrit approval type: %s',
73 approver['type'])
74 continue
75 cat = cats[0].strip()
76 val = int(approver['value'])
77 if not cat in approvs:
78 # Ignore the extended categories in the summary view.
79 continue
80 elif approvs[cat] is '':
81 approvs[cat] = val
82 elif val < 0:
83 approvs[cat] = min(approvs[cat], val)
84 else:
85 approvs[cat] = max(approvs[cat], val)
86 return approvs
87
88
89def PrintCl(opts, cls, lims, show_approvals=True):
90 """Pretty print a single result"""
91 if not lims:
92 lims = {'url': 0, 'project': 0}
93
94 status = ''
95 if show_approvals and not opts.verbose:
96 approvs = GetApprovalSummary(opts, cls)
97 for cat in GERRIT_SUMMARY_CATS:
98 if approvs[cat] is '':
99 functor = lambda x: x
100 elif approvs[cat] < 0:
101 functor = red
102 else:
103 functor = green
104 status += functor('%s:%2s ' % (cat, approvs[cat]))
105
106 print '%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cls['url'])), status,
107 lims['project'], cls['project'], cls['subject'])
108
109 if show_approvals and opts.verbose:
110 for approver in cls['currentPatchSet'].get('approvals', []):
111 functor = red if int(approver['value']) < 0 else green
112 n = functor('%2s' % approver['value'])
113 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
114 approver['type']])[1]
115 print ' %s %s %s' % (n, t, approver['by']['email'])
116
117
118def _MyUserInfo():
119 username = os.environ['USER']
120 emails = ['%s@%s' % (username, domain)
121 for domain in ('google.com', 'chromium.org')]
122 reviewers = ['reviewer:%s' % x for x in emails]
123 owners = ['owner:%s' % x for x in emails]
124 return emails, reviewers, owners
125
126
127def FilteredQuery(opts, query):
128 """Query gerrit and filter/clean up the results"""
129 ret = []
130
131 for cl in opts.gerrit.Query(query, raw=True):
132 # Gerrit likes to return a stats record too.
133 if not 'project' in cl:
134 continue
135
136 # Strip off common leading names since the result is still
137 # unique over the whole tree.
138 if not opts.verbose:
139 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform'):
140 if cl['project'].startswith('%s/' % pfx):
141 cl['project'] = cl['project'][len(pfx) + 1:]
142
143 ret.append(cl)
144
145 if opts.sort in ('number',):
146 key = lambda x: int(x[opts.sort])
147 else:
148 key = lambda x: x[opts.sort]
149 return sorted(ret, key=key)
150
151
152def ChangeNumberToCommit(opts, idx):
153 """Given a gerrit CL #, return the revision info
154
155 This is the form that the gerrit ssh interface expects.
156 """
157 cl = opts.gerrit.QuerySingleRecord(idx, raw=True)
158 return cl['currentPatchSet']['revision']
159
160
Mike Frysinger13f23a42013-05-13 17:32:01 -0400161def IsApprover(cl, users):
162 """See if the approvers in |cl| is listed in |users|"""
163 # See if we are listed in the approvals list. We have to parse
164 # this by hand as the gerrit query system doesn't support it :(
165 # http://code.google.com/p/gerrit/issues/detail?id=1235
166 if 'approvals' not in cl['currentPatchSet']:
167 return False
168
169 if isinstance(users, basestring):
170 users = (users,)
171
172 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700173 if (approver['by']['email'] in users and
174 approver['type'] == 'CRVW' and
175 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400176 return True
177
178 return False
179
180
181def UserActTodo(opts):
182 """List CLs needing your review"""
183 emails, reviewers, owners = _MyUserInfo()
184 cls = FilteredQuery(opts, '( %s ) status:open NOT ( %s )' %
185 (' OR '.join(reviewers), ' OR '.join(owners)))
186 cls = [x for x in cls if not IsApprover(x, emails)]
187 lims = limits(cls)
188 for cl in cls:
189 PrintCl(opts, cl, lims)
190
191
192def UserActMine(opts):
193 """List your CLs with review statuses"""
194 _, _, owners = _MyUserInfo()
195 cls = FilteredQuery(opts, '( %s ) status:new' % (' OR '.join(owners),))
196 lims = limits(cls)
197 for cl in cls:
198 PrintCl(opts, cl, lims)
199
200
201def UserActInspect(opts, idx):
202 """Inspect CL number <n>"""
203 PrintCl(opts, FilteredQuery(opts, idx)[0], None)
204
205
206def UserActReview(opts, idx, num):
207 """Mark CL <n> with code review status [-2,-1,0,1,2]"""
Stefan Zager29560302013-09-06 14:30:54 -0700208 opts.gerrit.SetReview(idx, labels={'Code-Review': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400209
210
211def UserActVerify(opts, idx, num):
212 """Mark CL <n> with verify status [-1,0,1]"""
Stefan Zager29560302013-09-06 14:30:54 -0700213 opts.gerrit.SetReview(idx, labels={'Verified': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400214
215
216def UserActReady(opts, idx, num):
217 """Mark CL <n> with ready status [-1,0,1]"""
Stefan Zager29560302013-09-06 14:30:54 -0700218 opts.gerrit.SetReview(idx, labels={'Commit-Queue': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400219
220
221def UserActSubmit(opts, idx):
222 """Submit CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700223 opts.gerrit.SubmitChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400224
225
226def UserActAbandon(opts, idx):
227 """Abandon CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700228 opts.gerrit.AbandonChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400229
230
231def UserActRestore(opts, idx):
232 """Restore CL <n> that was abandoned"""
Stefan Zager29560302013-09-06 14:30:54 -0700233 opts.gerrit.RestoreChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400234
235
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700236def UserActReviewers(opts, idx, *emails):
237 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
238
239 # Allow for optional leading '~'.
240 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
241 add_list, remove_list, invalid_list = [], [], []
242
243 for x in emails:
244 if not email_validator.match(x):
245 invalid_list.append(x)
246 elif x[0] == '~':
247 remove_list.append(x[1:])
248 else:
249 add_list.append(x)
250
251 if invalid_list:
252 cros_build_lib.Die(
253 'Invalid email address(es): %s' % ', '.join(invalid_list))
254
255 if add_list or remove_list:
256 opts.gerrit.SetReviewers(idx, add=add_list, remove=remove_list)
257
258
Doug Anderson8119df02013-07-20 21:00:24 +0530259def UserActMessage(opts, idx, message):
260 """Add a message to CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700261 opts.gerrit.SetReview(idx, msg=message)
Doug Anderson8119df02013-07-20 21:00:24 +0530262
263
Mike Frysinger13f23a42013-05-13 17:32:01 -0400264def main(argv):
265 # Locate actions that are exposed to the user. All functions that start
266 # with "UserAct" are fair game.
267 act_pfx = 'UserAct'
268 actions = [x for x in globals() if x.startswith(act_pfx)]
269
270 usage = """%prog [options] <action> [action args]
271
272There is no support for doing line-by-line code review via the command line.
273This helps you manage various bits and CL status.
274
275Example:
276 $ gerrit todo # List all the CLs that await your review.
277 $ gerrit mine # List all of your open CLs.
278 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
279 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
280 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
281
282Actions:"""
283 indent = max([len(x) - len(act_pfx) for x in actions])
284 for a in sorted(actions):
285 usage += '\n %-*s: %s' % (indent, a[len(act_pfx):].lower(),
286 globals()[a].__doc__)
287
288 parser = commandline.OptionParser(usage=usage)
289 parser.add_option('-i', '--internal', default=None, action='store_true',
290 help='Query gerrit-int')
291 parser.add_option('-e', '--external', dest='internal', action='store_false',
292 help='Query gerrit (default)')
293 parser.add_option('--sort', default='number', help='Key to sort on '
294 '(number, project)')
295 parser.add_option('-v', '--verbose', default=False, action='store_true',
296 help='Be more verbose in output')
297 opts, args = parser.parse_args(argv)
298 if not args:
299 parser.error('missing action')
300
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400301 # pylint: disable=W0603
302 global COLOR
303 COLOR = terminal.Color(enabled=opts.color)
304
Mike Frysinger13f23a42013-05-13 17:32:01 -0400305 # TODO: This sucks. We assume that all actions which take an argument are
306 # a CL #. Or at least, there's no other reason for it to start with a *.
307 # We do this to automatically select internal vs external gerrit as this
308 # convention is encoded in much of our system. However, the rest of this
309 # script doesn't expect (or want) the leading *.
310 if len(args) > 1:
311 if args[1][0] == '*':
312 if opts.internal is None:
313 opts.internal = True
314 args[1] = args[1][1:]
315
Stefan Zageraf6c6142013-05-28 17:25:41 -0700316 opts.gerrit = gerrit.GetGerritHelper(
Mike Frysinger13f23a42013-05-13 17:32:01 -0400317 constants.INTERNAL_REMOTE if opts.internal else constants.EXTERNAL_REMOTE,
318 print_cmd=opts.debug)
319
320 # Now look up the requested user action and run it.
321 cmd = args[0].lower()
322 args = args[1:]
323 functor = globals().get(act_pfx + cmd.capitalize())
324 if functor:
325 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700326 if argspec.varargs:
327 if len(args) < len(argspec.args):
328 parser.error('incorrect number of args: %s expects at least %s' %
329 (cmd, len(argspec.args)))
330 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400331 parser.error('incorrect number of args: %s expects %s' %
332 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700333 try:
334 functor(opts, *args)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700335 except (cros_build_lib.RunCommandError, gerrit.GerritException) as e:
336 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400337 else:
338 parser.error('unknown action: %s' % (cmd,))