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