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