blob: c3eeb93e0a91b0ed82782b860c5a037d7e4ba851 [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():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040061 # Use %s rather than str() to avoid codec issues.
62 # We also do this so we can format integers.
63 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040064 return lims
65
66
67def GetApprovalSummary(_opts, cls):
68 """Return a dict of the most important approvals"""
69 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
70 if 'approvals' in cls['currentPatchSet']:
71 for approver in cls['currentPatchSet']['approvals']:
72 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
73 if not cats:
74 cros_build_lib.Warning('unknown gerrit approval type: %s',
75 approver['type'])
76 continue
77 cat = cats[0].strip()
78 val = int(approver['value'])
79 if not cat in approvs:
80 # Ignore the extended categories in the summary view.
81 continue
82 elif approvs[cat] is '':
83 approvs[cat] = val
84 elif val < 0:
85 approvs[cat] = min(approvs[cat], val)
86 else:
87 approvs[cat] = max(approvs[cat], val)
88 return approvs
89
90
91def PrintCl(opts, cls, lims, show_approvals=True):
92 """Pretty print a single result"""
93 if not lims:
94 lims = {'url': 0, 'project': 0}
95
96 status = ''
97 if show_approvals and not opts.verbose:
98 approvs = GetApprovalSummary(opts, cls)
99 for cat in GERRIT_SUMMARY_CATS:
100 if approvs[cat] is '':
101 functor = lambda x: x
102 elif approvs[cat] < 0:
103 functor = red
104 else:
105 functor = green
106 status += functor('%s:%2s ' % (cat, approvs[cat]))
107
108 print '%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cls['url'])), status,
109 lims['project'], cls['project'], cls['subject'])
110
111 if show_approvals and opts.verbose:
112 for approver in cls['currentPatchSet'].get('approvals', []):
113 functor = red if int(approver['value']) < 0 else green
114 n = functor('%2s' % approver['value'])
115 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
116 approver['type']])[1]
117 print ' %s %s %s' % (n, t, approver['by']['email'])
118
119
120def _MyUserInfo():
121 username = os.environ['USER']
122 emails = ['%s@%s' % (username, domain)
123 for domain in ('google.com', 'chromium.org')]
124 reviewers = ['reviewer:%s' % x for x in emails]
125 owners = ['owner:%s' % x for x in emails]
126 return emails, reviewers, owners
127
128
129def FilteredQuery(opts, query):
130 """Query gerrit and filter/clean up the results"""
131 ret = []
132
133 for cl in opts.gerrit.Query(query, raw=True):
134 # Gerrit likes to return a stats record too.
135 if not 'project' in cl:
136 continue
137
138 # Strip off common leading names since the result is still
139 # unique over the whole tree.
140 if not opts.verbose:
141 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform'):
142 if cl['project'].startswith('%s/' % pfx):
143 cl['project'] = cl['project'][len(pfx) + 1:]
144
145 ret.append(cl)
146
147 if opts.sort in ('number',):
148 key = lambda x: int(x[opts.sort])
149 else:
150 key = lambda x: x[opts.sort]
151 return sorted(ret, key=key)
152
153
154def ChangeNumberToCommit(opts, idx):
155 """Given a gerrit CL #, return the revision info
156
157 This is the form that the gerrit ssh interface expects.
158 """
159 cl = opts.gerrit.QuerySingleRecord(idx, raw=True)
160 return cl['currentPatchSet']['revision']
161
162
Mike Frysinger13f23a42013-05-13 17:32:01 -0400163def IsApprover(cl, users):
164 """See if the approvers in |cl| is listed in |users|"""
165 # See if we are listed in the approvals list. We have to parse
166 # this by hand as the gerrit query system doesn't support it :(
167 # http://code.google.com/p/gerrit/issues/detail?id=1235
168 if 'approvals' not in cl['currentPatchSet']:
169 return False
170
171 if isinstance(users, basestring):
172 users = (users,)
173
174 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700175 if (approver['by']['email'] in users and
176 approver['type'] == 'CRVW' and
177 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400178 return True
179
180 return False
181
182
183def UserActTodo(opts):
184 """List CLs needing your review"""
185 emails, reviewers, owners = _MyUserInfo()
186 cls = FilteredQuery(opts, '( %s ) status:open NOT ( %s )' %
187 (' OR '.join(reviewers), ' OR '.join(owners)))
188 cls = [x for x in cls if not IsApprover(x, emails)]
189 lims = limits(cls)
190 for cl in cls:
191 PrintCl(opts, cl, lims)
192
193
194def UserActMine(opts):
195 """List your CLs with review statuses"""
196 _, _, owners = _MyUserInfo()
197 cls = FilteredQuery(opts, '( %s ) status:new' % (' OR '.join(owners),))
198 lims = limits(cls)
199 for cl in cls:
200 PrintCl(opts, cl, lims)
201
202
203def UserActInspect(opts, idx):
204 """Inspect CL number <n>"""
205 PrintCl(opts, FilteredQuery(opts, idx)[0], None)
206
207
208def UserActReview(opts, idx, num):
209 """Mark CL <n> with code review status [-2,-1,0,1,2]"""
Stefan Zager29560302013-09-06 14:30:54 -0700210 opts.gerrit.SetReview(idx, labels={'Code-Review': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400211
212
213def UserActVerify(opts, idx, num):
214 """Mark CL <n> with verify status [-1,0,1]"""
Stefan Zager29560302013-09-06 14:30:54 -0700215 opts.gerrit.SetReview(idx, labels={'Verified': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400216
217
218def UserActReady(opts, idx, num):
219 """Mark CL <n> with ready status [-1,0,1]"""
Stefan Zager29560302013-09-06 14:30:54 -0700220 opts.gerrit.SetReview(idx, labels={'Commit-Queue': num})
Mike Frysinger13f23a42013-05-13 17:32:01 -0400221
222
223def UserActSubmit(opts, idx):
224 """Submit CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700225 opts.gerrit.SubmitChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400226
227
228def UserActAbandon(opts, idx):
229 """Abandon CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700230 opts.gerrit.AbandonChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400231
232
233def UserActRestore(opts, idx):
234 """Restore CL <n> that was abandoned"""
Stefan Zager29560302013-09-06 14:30:54 -0700235 opts.gerrit.RestoreChange(idx)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400236
237
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700238def UserActReviewers(opts, idx, *emails):
239 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
240
241 # Allow for optional leading '~'.
242 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
243 add_list, remove_list, invalid_list = [], [], []
244
245 for x in emails:
246 if not email_validator.match(x):
247 invalid_list.append(x)
248 elif x[0] == '~':
249 remove_list.append(x[1:])
250 else:
251 add_list.append(x)
252
253 if invalid_list:
254 cros_build_lib.Die(
255 'Invalid email address(es): %s' % ', '.join(invalid_list))
256
257 if add_list or remove_list:
258 opts.gerrit.SetReviewers(idx, add=add_list, remove=remove_list)
259
260
Doug Anderson8119df02013-07-20 21:00:24 +0530261def UserActMessage(opts, idx, message):
262 """Add a message to CL <n>"""
Stefan Zager29560302013-09-06 14:30:54 -0700263 opts.gerrit.SetReview(idx, msg=message)
Doug Anderson8119df02013-07-20 21:00:24 +0530264
265
Mike Frysinger13f23a42013-05-13 17:32:01 -0400266def main(argv):
267 # Locate actions that are exposed to the user. All functions that start
268 # with "UserAct" are fair game.
269 act_pfx = 'UserAct'
270 actions = [x for x in globals() if x.startswith(act_pfx)]
271
272 usage = """%prog [options] <action> [action args]
273
274There is no support for doing line-by-line code review via the command line.
275This helps you manage various bits and CL status.
276
277Example:
278 $ gerrit todo # List all the CLs that await your review.
279 $ gerrit mine # List all of your open CLs.
280 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
281 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
282 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
283
284Actions:"""
285 indent = max([len(x) - len(act_pfx) for x in actions])
286 for a in sorted(actions):
287 usage += '\n %-*s: %s' % (indent, a[len(act_pfx):].lower(),
288 globals()[a].__doc__)
289
290 parser = commandline.OptionParser(usage=usage)
291 parser.add_option('-i', '--internal', default=None, action='store_true',
292 help='Query gerrit-int')
293 parser.add_option('-e', '--external', dest='internal', action='store_false',
294 help='Query gerrit (default)')
295 parser.add_option('--sort', default='number', help='Key to sort on '
296 '(number, project)')
297 parser.add_option('-v', '--verbose', default=False, action='store_true',
298 help='Be more verbose in output')
299 opts, args = parser.parse_args(argv)
300 if not args:
301 parser.error('missing action')
302
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400303 # pylint: disable=W0603
304 global COLOR
305 COLOR = terminal.Color(enabled=opts.color)
306
Mike Frysinger13f23a42013-05-13 17:32:01 -0400307 # TODO: This sucks. We assume that all actions which take an argument are
308 # a CL #. Or at least, there's no other reason for it to start with a *.
309 # We do this to automatically select internal vs external gerrit as this
310 # convention is encoded in much of our system. However, the rest of this
311 # script doesn't expect (or want) the leading *.
312 if len(args) > 1:
313 if args[1][0] == '*':
314 if opts.internal is None:
315 opts.internal = True
316 args[1] = args[1][1:]
317
Stefan Zageraf6c6142013-05-28 17:25:41 -0700318 opts.gerrit = gerrit.GetGerritHelper(
Mike Frysinger13f23a42013-05-13 17:32:01 -0400319 constants.INTERNAL_REMOTE if opts.internal else constants.EXTERNAL_REMOTE,
320 print_cmd=opts.debug)
321
322 # Now look up the requested user action and run it.
323 cmd = args[0].lower()
324 args = args[1:]
325 functor = globals().get(act_pfx + cmd.capitalize())
326 if functor:
327 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700328 if argspec.varargs:
329 if len(args) < len(argspec.args):
330 parser.error('incorrect number of args: %s expects at least %s' %
331 (cmd, len(argspec.args)))
332 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400333 parser.error('incorrect number of args: %s expects %s' %
334 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700335 try:
336 functor(opts, *args)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700337 except (cros_build_lib.RunCommandError, gerrit.GerritException) as e:
338 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400339 else:
340 parser.error('unknown action: %s' % (cmd,))