blob: 093c5d16e5e54ec57ec49c417c700f3bf7bd881e [file] [log] [blame]
Mike Frysinger13f23a42013-05-13 17:32:01 -04001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Mike Frysinger08737512014-02-07 22:58:26 -05005"""A command line interface to Gerrit-on-borg instances.
Mike Frysinger13f23a42013-05-13 17:32:01 -04006
7Internal Note:
8To expose a function directly to the command line interface, name your function
9with the prefix "UserAct".
10"""
11
Mike Frysinger31ff6f92014-02-08 04:33:03 -050012from __future__ import print_function
13
Mike Frysinger13f23a42013-05-13 17:32:01 -040014import inspect
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -080015import pprint
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070016import re
Mike Frysinger13f23a42013-05-13 17:32:01 -040017
Don Garrett88b8d782014-05-13 17:30:55 -070018from chromite.cbuildbot import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040019from chromite.lib import commandline
20from chromite.lib import cros_build_lib
21from chromite.lib import gerrit
Mathieu Olivari04b4d522014-12-18 17:26:34 -080022from chromite.lib import git
Mike Frysingerc85d8162014-02-08 00:45:21 -050023from chromite.lib import gob_util
Mike Frysinger13f23a42013-05-13 17:32:01 -040024from chromite.lib import terminal
25
26
Mike Frysinger031ad0b2013-05-14 18:15:34 -040027COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040028
29# Map the internal names to the ones we normally show on the web ui.
30GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080031 'COMR': ['CQ', 'Commit Queue ',],
32 'CRVW': ['CR', 'Code Review ',],
33 'SUBM': ['S ', 'Submitted ',],
David James2b2e2c52014-12-02 19:32:07 -080034 'TRY': ['T ', 'Trybot Ready ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080035 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040036}
37
38# Order is important -- matches the web ui. This also controls the short
39# entries that we summarize in non-verbose mode.
40GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
41
42
43def red(s):
44 return COLOR.Color(terminal.Color.RED, s)
45
46
47def green(s):
48 return COLOR.Color(terminal.Color.GREEN, s)
49
50
51def blue(s):
52 return COLOR.Color(terminal.Color.BLUE, s)
53
54
55def limits(cls):
56 """Given a dict of fields, calculate the longest string lengths
57
58 This allows you to easily format the output of many results so that the
59 various cols all line up correctly.
60 """
61 lims = {}
62 for cl in cls:
63 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040064 # Use %s rather than str() to avoid codec issues.
65 # We also do this so we can format integers.
66 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040067 return lims
68
69
Mike Frysinger88f27292014-06-17 09:40:45 -070070# TODO: This func really needs to be merged into the core gerrit logic.
71def GetGerrit(opts, cl=None):
72 """Auto pick the right gerrit instance based on the |cl|
73
74 Args:
75 opts: The general options object.
76 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
77
78 Returns:
79 A tuple of a gerrit object and a sanitized CL #.
80 """
81 gob = opts.gob
82 if not cl is None:
83 if cl.startswith('*'):
84 gob = constants.INTERNAL_GOB_INSTANCE
85 cl = cl[1:]
86 elif ':' in cl:
87 gob, cl = cl.split(':', 1)
88
89 if not gob in opts.gerrit:
90 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
91
92 return (opts.gerrit[gob], cl)
93
94
Mike Frysinger13f23a42013-05-13 17:32:01 -040095def GetApprovalSummary(_opts, cls):
96 """Return a dict of the most important approvals"""
97 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
98 if 'approvals' in cls['currentPatchSet']:
99 for approver in cls['currentPatchSet']['approvals']:
100 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
101 if not cats:
102 cros_build_lib.Warning('unknown gerrit approval type: %s',
103 approver['type'])
104 continue
105 cat = cats[0].strip()
106 val = int(approver['value'])
107 if not cat in approvs:
108 # Ignore the extended categories in the summary view.
109 continue
110 elif approvs[cat] is '':
111 approvs[cat] = val
112 elif val < 0:
113 approvs[cat] = min(approvs[cat], val)
114 else:
115 approvs[cat] = max(approvs[cat], val)
116 return approvs
117
118
119def PrintCl(opts, cls, lims, show_approvals=True):
120 """Pretty print a single result"""
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700121 if opts.raw:
122 # Special case internal Chrome GoB as that is what most devs use.
123 # They can always redirect the list elsewhere via the -g option.
124 if opts.gob == constants.INTERNAL_GOB_INSTANCE:
125 print(constants.INTERNAL_CHANGE_PREFIX, end='')
126 print(cls['number'])
127 return
128
Mike Frysinger13f23a42013-05-13 17:32:01 -0400129 if not lims:
130 lims = {'url': 0, 'project': 0}
131
132 status = ''
133 if show_approvals and not opts.verbose:
134 approvs = GetApprovalSummary(opts, cls)
135 for cat in GERRIT_SUMMARY_CATS:
136 if approvs[cat] is '':
137 functor = lambda x: x
138 elif approvs[cat] < 0:
139 functor = red
140 else:
141 functor = green
142 status += functor('%s:%2s ' % (cat, approvs[cat]))
143
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500144 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cls['url'])), status,
145 lims['project'], cls['project'], cls['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400146
147 if show_approvals and opts.verbose:
148 for approver in cls['currentPatchSet'].get('approvals', []):
149 functor = red if int(approver['value']) < 0 else green
150 n = functor('%2s' % approver['value'])
151 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
152 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500153 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400154
155
156def _MyUserInfo():
Mathieu Olivari04b4d522014-12-18 17:26:34 -0800157 email = git.GetProjectUserEmail(constants.CHROMITE_DIR)
158 [username, _, domain] = email.partition('@')
159 if domain in ('google.com', 'chromium.org'):
160 emails = ['%s@%s' % (username, domain)
161 for domain in ('google.com', 'chromium.org')]
162 else:
163 emails = [email]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400164 reviewers = ['reviewer:%s' % x for x in emails]
165 owners = ['owner:%s' % x for x in emails]
166 return emails, reviewers, owners
167
168
169def FilteredQuery(opts, query):
170 """Query gerrit and filter/clean up the results"""
171 ret = []
172
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800173 if opts.branch is not None:
174 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800175 if opts.project is not None:
176 query += ' project: %s' % opts.project
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800177
Mike Frysinger88f27292014-06-17 09:40:45 -0700178 helper, _ = GetGerrit(opts)
179 for cl in helper.Query(query, raw=True, bypass_cache=False):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400180 # Gerrit likes to return a stats record too.
181 if not 'project' in cl:
182 continue
183
184 # Strip off common leading names since the result is still
185 # unique over the whole tree.
186 if not opts.verbose:
Mike Frysingere5e78272014-06-15 00:41:30 -0700187 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform',
188 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400189 if cl['project'].startswith('%s/' % pfx):
190 cl['project'] = cl['project'][len(pfx) + 1:]
191
192 ret.append(cl)
193
194 if opts.sort in ('number',):
195 key = lambda x: int(x[opts.sort])
196 else:
197 key = lambda x: x[opts.sort]
198 return sorted(ret, key=key)
199
200
Mike Frysinger13f23a42013-05-13 17:32:01 -0400201def IsApprover(cl, users):
202 """See if the approvers in |cl| is listed in |users|"""
203 # See if we are listed in the approvals list. We have to parse
204 # this by hand as the gerrit query system doesn't support it :(
205 # http://code.google.com/p/gerrit/issues/detail?id=1235
206 if 'approvals' not in cl['currentPatchSet']:
207 return False
208
209 if isinstance(users, basestring):
210 users = (users,)
211
212 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700213 if (approver['by']['email'] in users and
214 approver['type'] == 'CRVW' and
215 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400216 return True
217
218 return False
219
220
221def UserActTodo(opts):
222 """List CLs needing your review"""
223 emails, reviewers, owners = _MyUserInfo()
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500224 cls = FilteredQuery(opts, ('( %s ) status:open NOT ( %s )' %
225 (' OR '.join(reviewers), ' OR '.join(owners))))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400226 cls = [x for x in cls if not IsApprover(x, emails)]
227 lims = limits(cls)
228 for cl in cls:
229 PrintCl(opts, cl, lims)
230
231
Mike Frysingera1db2c42014-06-15 00:42:48 -0700232def UserActSearch(opts, query):
233 """List CLs matching the Gerrit <search query>"""
234 cls = FilteredQuery(opts, query)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400235 lims = limits(cls)
236 for cl in cls:
237 PrintCl(opts, cl, lims)
238
239
Mike Frysingera1db2c42014-06-15 00:42:48 -0700240def UserActMine(opts):
241 """List your CLs with review statuses"""
242 _, _, owners = _MyUserInfo()
243 UserActSearch(opts, '( %s ) status:new' % (' OR '.join(owners),))
244
245
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700246def UserActInspect(opts, *args):
247 """Inspect CL number <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700248 for arg in args:
249 cl = FilteredQuery(opts, arg)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700250 if cl:
251 PrintCl(opts, cl[0], None)
252 else:
Mike Frysinger88f27292014-06-17 09:40:45 -0700253 print('no results found for CL %s' % arg)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400254
255
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700256def UserActReview(opts, *args):
257 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
258 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700259 for arg in args[:-1]:
260 helper, cl = GetGerrit(opts, arg)
261 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700262UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400263
264
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700265def UserActVerify(opts, *args):
266 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
267 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700268 for arg in args[:-1]:
269 helper, cl = GetGerrit(opts, arg)
270 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700271UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400272
273
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700274def UserActReady(opts, *args):
275 """Mark CL <n> [n ...] with ready status <0,1,2>"""
276 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700277 for arg in args[:-1]:
278 helper, cl = GetGerrit(opts, arg)
279 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700280UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400281
282
Mike Frysinger15b23e42014-12-05 17:00:05 -0500283def UserActTrybotready(opts, *args):
284 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
285 num = args[-1]
286 for arg in args[:-1]:
287 helper, cl = GetGerrit(opts, arg)
288 helper.SetReview(cl, labels={'Trybot-Ready': num}, dryrun=opts.dryrun)
289UserActTrybotready.arg_min = 2
290
291
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700292def UserActSubmit(opts, *args):
293 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700294 for arg in args:
295 helper, cl = GetGerrit(opts, arg)
296 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400297
298
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700299def UserActAbandon(opts, *args):
300 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700301 for arg in args:
302 helper, cl = GetGerrit(opts, arg)
303 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400304
305
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700306def UserActRestore(opts, *args):
307 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700308 for arg in args:
309 helper, cl = GetGerrit(opts, arg)
310 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400311
312
Mike Frysinger88f27292014-06-17 09:40:45 -0700313def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700314 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500315 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700316 # Allow for optional leading '~'.
317 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
318 add_list, remove_list, invalid_list = [], [], []
319
320 for x in emails:
321 if not email_validator.match(x):
322 invalid_list.append(x)
323 elif x[0] == '~':
324 remove_list.append(x[1:])
325 else:
326 add_list.append(x)
327
328 if invalid_list:
329 cros_build_lib.Die(
330 'Invalid email address(es): %s' % ', '.join(invalid_list))
331
332 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700333 helper, cl = GetGerrit(opts, cl)
334 helper.SetReviewers(cl, add=add_list, remove=remove_list,
335 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700336
337
Mike Frysinger88f27292014-06-17 09:40:45 -0700338def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530339 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700340 helper, cl = GetGerrit(opts, cl)
341 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530342
343
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800344def UserActTopic(opts, topic, *args):
345 """Set |topic| for CL number <n> [n ...]"""
346 for arg in args:
347 helper, arg = GetGerrit(opts, arg)
348 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
349
350
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700351def UserActDeletedraft(opts, *args):
352 """Delete draft patch set <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700353 for arg in args:
354 helper, cl = GetGerrit(opts, arg)
355 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800356
357
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800358def UserActAccount(opts):
359 """Get user account information."""
360 helper, _ = GetGerrit(opts)
361 pprint.PrettyPrinter().pprint(helper.GetAccount())
362
363
Mike Frysinger13f23a42013-05-13 17:32:01 -0400364def main(argv):
365 # Locate actions that are exposed to the user. All functions that start
366 # with "UserAct" are fair game.
367 act_pfx = 'UserAct'
368 actions = [x for x in globals() if x.startswith(act_pfx)]
369
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500370 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400371
372There is no support for doing line-by-line code review via the command line.
373This helps you manage various bits and CL status.
374
Mike Frysingera1db2c42014-06-15 00:42:48 -0700375For general Gerrit documentation, see:
376 https://gerrit-review.googlesource.com/Documentation/
377The Searching Changes page covers the search query syntax:
378 https://gerrit-review.googlesource.com/Documentation/user-search.html
379
Mike Frysinger13f23a42013-05-13 17:32:01 -0400380Example:
381 $ gerrit todo # List all the CLs that await your review.
382 $ gerrit mine # List all of your open CLs.
383 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
384 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
385 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700386Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700387 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
388ready.
389 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
390ready.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400391
392Actions:"""
393 indent = max([len(x) - len(act_pfx) for x in actions])
394 for a in sorted(actions):
Mike Frysinger15b23e42014-12-05 17:00:05 -0500395 cmd = a[len(act_pfx):]
396 # Sanity check for devs adding new commands. Should be quick.
397 if cmd != cmd.lower().capitalize():
398 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
399 (cmd, cmd.lower().capitalize()))
400 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400401
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500402 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500403 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Mike Frysinger88f27292014-06-17 09:40:45 -0700404 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysinger40541c62014-02-08 04:38:37 -0500405 const=constants.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500406 help='Query internal Chromium Gerrit instance')
407 parser.add_argument('-g', '--gob',
Mike Frysinger88f27292014-06-17 09:40:45 -0700408 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500409 help=('Gerrit (on borg) instance to query (default: %s)' %
410 (constants.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500411 parser.add_argument('--sort', default='number',
412 help='Key to sort on (number, project)')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700413 parser.add_argument('--raw', default=False, action='store_true',
414 help='Return raw results (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700415 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
416 dest='dryrun',
417 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500418 parser.add_argument('-v', '--verbose', default=False, action='store_true',
419 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800420 parser.add_argument('-b', '--branch',
421 help='Limit output to the specific branch')
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800422 parser.add_argument('-p', '--project',
423 help='Limit output to the specific project')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500424 parser.add_argument('args', nargs='+')
425 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400426
Mike Frysinger88f27292014-06-17 09:40:45 -0700427 # A cache of gerrit helpers we'll load on demand.
428 opts.gerrit = {}
429 opts.Freeze()
430
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400431 # pylint: disable=W0603
432 global COLOR
433 COLOR = terminal.Color(enabled=opts.color)
434
Mike Frysinger13f23a42013-05-13 17:32:01 -0400435 # Now look up the requested user action and run it.
Mike Frysinger88f27292014-06-17 09:40:45 -0700436 cmd = opts.args[0].lower()
437 args = opts.args[1:]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400438 functor = globals().get(act_pfx + cmd.capitalize())
439 if functor:
440 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700441 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700442 arg_min = getattr(functor, 'arg_min', len(argspec.args))
443 if len(args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700444 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700445 (cmd, arg_min))
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700446 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400447 parser.error('incorrect number of args: %s expects %s' %
448 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700449 try:
450 functor(opts, *args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500451 except (cros_build_lib.RunCommandError, gerrit.GerritException,
452 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700453 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400454 else:
455 parser.error('unknown action: %s' % (cmd,))