blob: 89fa29a624477e054d9772c44113adc1940e6164 [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 Bendebury614f8682013-05-23 10:33:35 -070016import sys
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
161def ReviewCommand(opts, idx, command):
162 """Shortcut to run `gerrit --review |command|` on a specific CL"""
163 rev = ChangeNumberToCommit(opts, idx)
164 cmd = opts.gerrit.GetGerritReviewCommand([rev] + command)
165 cros_build_lib.RunCommand(cmd, print_cmd=opts.debug)
166
167
168def IsApprover(cl, users):
169 """See if the approvers in |cl| is listed in |users|"""
170 # See if we are listed in the approvals list. We have to parse
171 # this by hand as the gerrit query system doesn't support it :(
172 # http://code.google.com/p/gerrit/issues/detail?id=1235
173 if 'approvals' not in cl['currentPatchSet']:
174 return False
175
176 if isinstance(users, basestring):
177 users = (users,)
178
179 for approver in cl['currentPatchSet']['approvals']:
180 if approver['by']['email'] in users:
181 return True
182
183 return False
184
185
186def UserActTodo(opts):
187 """List CLs needing your review"""
188 emails, reviewers, owners = _MyUserInfo()
189 cls = FilteredQuery(opts, '( %s ) status:open NOT ( %s )' %
190 (' OR '.join(reviewers), ' OR '.join(owners)))
191 cls = [x for x in cls if not IsApprover(x, emails)]
192 lims = limits(cls)
193 for cl in cls:
194 PrintCl(opts, cl, lims)
195
196
197def UserActMine(opts):
198 """List your CLs with review statuses"""
199 _, _, owners = _MyUserInfo()
200 cls = FilteredQuery(opts, '( %s ) status:new' % (' OR '.join(owners),))
201 lims = limits(cls)
202 for cl in cls:
203 PrintCl(opts, cl, lims)
204
205
206def UserActInspect(opts, idx):
207 """Inspect CL number <n>"""
208 PrintCl(opts, FilteredQuery(opts, idx)[0], None)
209
210
211def UserActReview(opts, idx, num):
212 """Mark CL <n> with code review status [-2,-1,0,1,2]"""
213 ReviewCommand(opts, idx, ['--code-review', str(num)])
214
215
216def UserActVerify(opts, idx, num):
217 """Mark CL <n> with verify status [-1,0,1]"""
218 ReviewCommand(opts, idx, ['--verified', str(num)])
219
220
221def UserActReady(opts, idx, num):
222 """Mark CL <n> with ready status [-1,0,1]"""
223 ReviewCommand(opts, idx, ['--commit-queue', str(num)])
224
225
226def UserActSubmit(opts, idx):
227 """Submit CL <n>"""
228 ReviewCommand(opts, idx, ['--submit'])
229
230
231def UserActAbandon(opts, idx):
232 """Abandon CL <n>"""
233 ReviewCommand(opts, idx, ['--abandon'])
234
235
236def UserActRestore(opts, idx):
237 """Restore CL <n> that was abandoned"""
238 ReviewCommand(opts, idx, ['--submit'])
239
240
241def main(argv):
242 # Locate actions that are exposed to the user. All functions that start
243 # with "UserAct" are fair game.
244 act_pfx = 'UserAct'
245 actions = [x for x in globals() if x.startswith(act_pfx)]
246
247 usage = """%prog [options] <action> [action args]
248
249There is no support for doing line-by-line code review via the command line.
250This helps you manage various bits and CL status.
251
252Example:
253 $ gerrit todo # List all the CLs that await your review.
254 $ gerrit mine # List all of your open CLs.
255 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
256 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
257 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
258
259Actions:"""
260 indent = max([len(x) - len(act_pfx) for x in actions])
261 for a in sorted(actions):
262 usage += '\n %-*s: %s' % (indent, a[len(act_pfx):].lower(),
263 globals()[a].__doc__)
264
265 parser = commandline.OptionParser(usage=usage)
266 parser.add_option('-i', '--internal', default=None, action='store_true',
267 help='Query gerrit-int')
268 parser.add_option('-e', '--external', dest='internal', action='store_false',
269 help='Query gerrit (default)')
270 parser.add_option('--sort', default='number', help='Key to sort on '
271 '(number, project)')
272 parser.add_option('-v', '--verbose', default=False, action='store_true',
273 help='Be more verbose in output')
274 opts, args = parser.parse_args(argv)
275 if not args:
276 parser.error('missing action')
277
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400278 # pylint: disable=W0603
279 global COLOR
280 COLOR = terminal.Color(enabled=opts.color)
281
Mike Frysinger13f23a42013-05-13 17:32:01 -0400282 # TODO: This sucks. We assume that all actions which take an argument are
283 # a CL #. Or at least, there's no other reason for it to start with a *.
284 # We do this to automatically select internal vs external gerrit as this
285 # convention is encoded in much of our system. However, the rest of this
286 # script doesn't expect (or want) the leading *.
287 if len(args) > 1:
288 if args[1][0] == '*':
289 if opts.internal is None:
290 opts.internal = True
291 args[1] = args[1][1:]
292
293 opts.gerrit = gerrit.GerritHelper.FromRemote(
294 constants.INTERNAL_REMOTE if opts.internal else constants.EXTERNAL_REMOTE,
295 print_cmd=opts.debug)
296
297 # Now look up the requested user action and run it.
298 cmd = args[0].lower()
299 args = args[1:]
300 functor = globals().get(act_pfx + cmd.capitalize())
301 if functor:
302 argspec = inspect.getargspec(functor)
303 if len(argspec.args) - 1 != len(args):
304 parser.error('incorrect number of args: %s expects %s' %
305 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700306 try:
307 functor(opts, *args)
308 except cros_build_lib.RunCommandError:
309 # An error message has been issued on stderr by now.
310 sys.exit(1)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400311 else:
312 parser.error('unknown action: %s' % (cmd,))