blob: ac73a032368de8d1e398409db98655d92e7ef32b [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
Doug Anderson8119df02013-07-20 21:00:24 +053016import pipes
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 = {
30 'COMR': ['CQ', 'Commit Queue',],
31 'CRVW': ['CR', 'Code Review ',],
32 'SUBM': ['S ', 'Submitted ',],
33 'VRIF': ['V ', 'Verified ',],
34}
35
36# Order is important -- matches the web ui. This also controls the short
37# entries that we summarize in non-verbose mode.
38GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
39
40
41def red(s):
42 return COLOR.Color(terminal.Color.RED, s)
43
44
45def green(s):
46 return COLOR.Color(terminal.Color.GREEN, s)
47
48
49def blue(s):
50 return COLOR.Color(terminal.Color.BLUE, s)
51
52
53def limits(cls):
54 """Given a dict of fields, calculate the longest string lengths
55
56 This allows you to easily format the output of many results so that the
57 various cols all line up correctly.
58 """
59 lims = {}
60 for cl in cls:
61 for k in cl.keys():
62 lims[k] = max(lims.get(k, 0), len(str(cl[k])))
63 return lims
64
65
66def GetApprovalSummary(_opts, cls):
67 """Return a dict of the most important approvals"""
68 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
69 if 'approvals' in cls['currentPatchSet']:
70 for approver in cls['currentPatchSet']['approvals']:
71 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
72 if not cats:
73 cros_build_lib.Warning('unknown gerrit approval type: %s',
74 approver['type'])
75 continue
76 cat = cats[0].strip()
77 val = int(approver['value'])
78 if not cat in approvs:
79 # Ignore the extended categories in the summary view.
80 continue
81 elif approvs[cat] is '':
82 approvs[cat] = val
83 elif val < 0:
84 approvs[cat] = min(approvs[cat], val)
85 else:
86 approvs[cat] = max(approvs[cat], val)
87 return approvs
88
89
90def PrintCl(opts, cls, lims, show_approvals=True):
91 """Pretty print a single result"""
92 if not lims:
93 lims = {'url': 0, 'project': 0}
94
95 status = ''
96 if show_approvals and not opts.verbose:
97 approvs = GetApprovalSummary(opts, cls)
98 for cat in GERRIT_SUMMARY_CATS:
99 if approvs[cat] is '':
100 functor = lambda x: x
101 elif approvs[cat] < 0:
102 functor = red
103 else:
104 functor = green
105 status += functor('%s:%2s ' % (cat, approvs[cat]))
106
107 print '%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cls['url'])), status,
108 lims['project'], cls['project'], cls['subject'])
109
110 if show_approvals and opts.verbose:
111 for approver in cls['currentPatchSet'].get('approvals', []):
112 functor = red if int(approver['value']) < 0 else green
113 n = functor('%2s' % approver['value'])
114 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
115 approver['type']])[1]
116 print ' %s %s %s' % (n, t, approver['by']['email'])
117
118
119def _MyUserInfo():
120 username = os.environ['USER']
121 emails = ['%s@%s' % (username, domain)
122 for domain in ('google.com', 'chromium.org')]
123 reviewers = ['reviewer:%s' % x for x in emails]
124 owners = ['owner:%s' % x for x in emails]
125 return emails, reviewers, owners
126
127
128def FilteredQuery(opts, query):
129 """Query gerrit and filter/clean up the results"""
130 ret = []
131
132 for cl in opts.gerrit.Query(query, raw=True):
133 # Gerrit likes to return a stats record too.
134 if not 'project' in cl:
135 continue
136
137 # Strip off common leading names since the result is still
138 # unique over the whole tree.
139 if not opts.verbose:
140 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform'):
141 if cl['project'].startswith('%s/' % pfx):
142 cl['project'] = cl['project'][len(pfx) + 1:]
143
144 ret.append(cl)
145
146 if opts.sort in ('number',):
147 key = lambda x: int(x[opts.sort])
148 else:
149 key = lambda x: x[opts.sort]
150 return sorted(ret, key=key)
151
152
153def ChangeNumberToCommit(opts, idx):
154 """Given a gerrit CL #, return the revision info
155
156 This is the form that the gerrit ssh interface expects.
157 """
158 cl = opts.gerrit.QuerySingleRecord(idx, raw=True)
159 return cl['currentPatchSet']['revision']
160
161
162def ReviewCommand(opts, idx, command):
163 """Shortcut to run `gerrit --review |command|` on a specific CL"""
164 rev = ChangeNumberToCommit(opts, idx)
165 cmd = opts.gerrit.GetGerritReviewCommand([rev] + command)
166 cros_build_lib.RunCommand(cmd, print_cmd=opts.debug)
167
168
169def IsApprover(cl, users):
170 """See if the approvers in |cl| is listed in |users|"""
171 # See if we are listed in the approvals list. We have to parse
172 # this by hand as the gerrit query system doesn't support it :(
173 # http://code.google.com/p/gerrit/issues/detail?id=1235
174 if 'approvals' not in cl['currentPatchSet']:
175 return False
176
177 if isinstance(users, basestring):
178 users = (users,)
179
180 for approver in cl['currentPatchSet']['approvals']:
181 if approver['by']['email'] in users:
182 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
198def UserActMine(opts):
199 """List your CLs with review statuses"""
200 _, _, owners = _MyUserInfo()
201 cls = FilteredQuery(opts, '( %s ) status:new' % (' OR '.join(owners),))
202 lims = limits(cls)
203 for cl in cls:
204 PrintCl(opts, cl, lims)
205
206
207def UserActInspect(opts, idx):
208 """Inspect CL number <n>"""
209 PrintCl(opts, FilteredQuery(opts, idx)[0], None)
210
211
212def UserActReview(opts, idx, num):
213 """Mark CL <n> with code review status [-2,-1,0,1,2]"""
214 ReviewCommand(opts, idx, ['--code-review', str(num)])
215
216
217def UserActVerify(opts, idx, num):
218 """Mark CL <n> with verify status [-1,0,1]"""
219 ReviewCommand(opts, idx, ['--verified', str(num)])
220
221
222def UserActReady(opts, idx, num):
223 """Mark CL <n> with ready status [-1,0,1]"""
224 ReviewCommand(opts, idx, ['--commit-queue', str(num)])
225
226
227def UserActSubmit(opts, idx):
228 """Submit CL <n>"""
229 ReviewCommand(opts, idx, ['--submit'])
230
231
232def UserActAbandon(opts, idx):
233 """Abandon CL <n>"""
234 ReviewCommand(opts, idx, ['--abandon'])
235
236
237def UserActRestore(opts, idx):
238 """Restore CL <n> that was abandoned"""
239 ReviewCommand(opts, idx, ['--submit'])
240
241
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700242def UserActReviewers(opts, idx, *emails):
243 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
244
245 # Allow for optional leading '~'.
246 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
247 add_list, remove_list, invalid_list = [], [], []
248
249 for x in emails:
250 if not email_validator.match(x):
251 invalid_list.append(x)
252 elif x[0] == '~':
253 remove_list.append(x[1:])
254 else:
255 add_list.append(x)
256
257 if invalid_list:
258 cros_build_lib.Die(
259 'Invalid email address(es): %s' % ', '.join(invalid_list))
260
261 if add_list or remove_list:
262 opts.gerrit.SetReviewers(idx, add=add_list, remove=remove_list)
263
264
Doug Anderson8119df02013-07-20 21:00:24 +0530265def UserActMessage(opts, idx, message):
266 """Add a message to CL <n>"""
267 ReviewCommand(opts, idx, ['--message', pipes.quote(message)])
268
269
Mike Frysinger13f23a42013-05-13 17:32:01 -0400270def main(argv):
271 # Locate actions that are exposed to the user. All functions that start
272 # with "UserAct" are fair game.
273 act_pfx = 'UserAct'
274 actions = [x for x in globals() if x.startswith(act_pfx)]
275
276 usage = """%prog [options] <action> [action args]
277
278There is no support for doing line-by-line code review via the command line.
279This helps you manage various bits and CL status.
280
281Example:
282 $ gerrit todo # List all the CLs that await your review.
283 $ gerrit mine # List all of your open CLs.
284 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
285 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
286 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
287
288Actions:"""
289 indent = max([len(x) - len(act_pfx) for x in actions])
290 for a in sorted(actions):
291 usage += '\n %-*s: %s' % (indent, a[len(act_pfx):].lower(),
292 globals()[a].__doc__)
293
294 parser = commandline.OptionParser(usage=usage)
295 parser.add_option('-i', '--internal', default=None, action='store_true',
296 help='Query gerrit-int')
297 parser.add_option('-e', '--external', dest='internal', action='store_false',
298 help='Query gerrit (default)')
299 parser.add_option('--sort', default='number', help='Key to sort on '
300 '(number, project)')
301 parser.add_option('-v', '--verbose', default=False, action='store_true',
302 help='Be more verbose in output')
303 opts, args = parser.parse_args(argv)
304 if not args:
305 parser.error('missing action')
306
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400307 # pylint: disable=W0603
308 global COLOR
309 COLOR = terminal.Color(enabled=opts.color)
310
Mike Frysinger13f23a42013-05-13 17:32:01 -0400311 # TODO: This sucks. We assume that all actions which take an argument are
312 # a CL #. Or at least, there's no other reason for it to start with a *.
313 # We do this to automatically select internal vs external gerrit as this
314 # convention is encoded in much of our system. However, the rest of this
315 # script doesn't expect (or want) the leading *.
316 if len(args) > 1:
317 if args[1][0] == '*':
318 if opts.internal is None:
319 opts.internal = True
320 args[1] = args[1][1:]
321
Stefan Zageraf6c6142013-05-28 17:25:41 -0700322 opts.gerrit = gerrit.GetGerritHelper(
Mike Frysinger13f23a42013-05-13 17:32:01 -0400323 constants.INTERNAL_REMOTE if opts.internal else constants.EXTERNAL_REMOTE,
324 print_cmd=opts.debug)
325
326 # Now look up the requested user action and run it.
327 cmd = args[0].lower()
328 args = args[1:]
329 functor = globals().get(act_pfx + cmd.capitalize())
330 if functor:
331 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700332 if argspec.varargs:
333 if len(args) < len(argspec.args):
334 parser.error('incorrect number of args: %s expects at least %s' %
335 (cmd, len(argspec.args)))
336 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400337 parser.error('incorrect number of args: %s expects %s' %
338 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700339 try:
340 functor(opts, *args)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700341 except (cros_build_lib.RunCommandError, gerrit.GerritException) as e:
342 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400343 else:
344 parser.error('unknown action: %s' % (cmd,))