blob: 8fb19258331e54a3e4421dcc9ded05a5878b192e [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
175
Mike Frysinger88f27292014-06-17 09:40:45 -0700176 helper, _ = GetGerrit(opts)
177 for cl in helper.Query(query, raw=True, bypass_cache=False):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400178 # Gerrit likes to return a stats record too.
179 if not 'project' in cl:
180 continue
181
182 # Strip off common leading names since the result is still
183 # unique over the whole tree.
184 if not opts.verbose:
Mike Frysingere5e78272014-06-15 00:41:30 -0700185 for pfx in ('chromeos', 'chromiumos', 'overlays', 'platform',
186 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400187 if cl['project'].startswith('%s/' % pfx):
188 cl['project'] = cl['project'][len(pfx) + 1:]
189
190 ret.append(cl)
191
192 if opts.sort in ('number',):
193 key = lambda x: int(x[opts.sort])
194 else:
195 key = lambda x: x[opts.sort]
196 return sorted(ret, key=key)
197
198
Mike Frysinger13f23a42013-05-13 17:32:01 -0400199def IsApprover(cl, users):
200 """See if the approvers in |cl| is listed in |users|"""
201 # See if we are listed in the approvals list. We have to parse
202 # this by hand as the gerrit query system doesn't support it :(
203 # http://code.google.com/p/gerrit/issues/detail?id=1235
204 if 'approvals' not in cl['currentPatchSet']:
205 return False
206
207 if isinstance(users, basestring):
208 users = (users,)
209
210 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700211 if (approver['by']['email'] in users and
212 approver['type'] == 'CRVW' and
213 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400214 return True
215
216 return False
217
218
219def UserActTodo(opts):
220 """List CLs needing your review"""
221 emails, reviewers, owners = _MyUserInfo()
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500222 cls = FilteredQuery(opts, ('( %s ) status:open NOT ( %s )' %
223 (' OR '.join(reviewers), ' OR '.join(owners))))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400224 cls = [x for x in cls if not IsApprover(x, emails)]
225 lims = limits(cls)
226 for cl in cls:
227 PrintCl(opts, cl, lims)
228
229
Mike Frysingera1db2c42014-06-15 00:42:48 -0700230def UserActSearch(opts, query):
231 """List CLs matching the Gerrit <search query>"""
232 cls = FilteredQuery(opts, query)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400233 lims = limits(cls)
234 for cl in cls:
235 PrintCl(opts, cl, lims)
236
237
Mike Frysingera1db2c42014-06-15 00:42:48 -0700238def UserActMine(opts):
239 """List your CLs with review statuses"""
240 _, _, owners = _MyUserInfo()
241 UserActSearch(opts, '( %s ) status:new' % (' OR '.join(owners),))
242
243
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700244def UserActInspect(opts, *args):
245 """Inspect CL number <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700246 for arg in args:
247 cl = FilteredQuery(opts, arg)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700248 if cl:
249 PrintCl(opts, cl[0], None)
250 else:
Mike Frysinger88f27292014-06-17 09:40:45 -0700251 print('no results found for CL %s' % arg)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400252
253
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700254def UserActReview(opts, *args):
255 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
256 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700257 for arg in args[:-1]:
258 helper, cl = GetGerrit(opts, arg)
259 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700260UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400261
262
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700263def UserActVerify(opts, *args):
264 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
265 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700266 for arg in args[:-1]:
267 helper, cl = GetGerrit(opts, arg)
268 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700269UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400270
271
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700272def UserActReady(opts, *args):
273 """Mark CL <n> [n ...] with ready status <0,1,2>"""
274 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700275 for arg in args[:-1]:
276 helper, cl = GetGerrit(opts, arg)
277 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700278UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400279
280
Mike Frysinger15b23e42014-12-05 17:00:05 -0500281def UserActTrybotready(opts, *args):
282 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
283 num = args[-1]
284 for arg in args[:-1]:
285 helper, cl = GetGerrit(opts, arg)
286 helper.SetReview(cl, labels={'Trybot-Ready': num}, dryrun=opts.dryrun)
287UserActTrybotready.arg_min = 2
288
289
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700290def UserActSubmit(opts, *args):
291 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700292 for arg in args:
293 helper, cl = GetGerrit(opts, arg)
294 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400295
296
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700297def UserActAbandon(opts, *args):
298 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700299 for arg in args:
300 helper, cl = GetGerrit(opts, arg)
301 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400302
303
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700304def UserActRestore(opts, *args):
305 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700306 for arg in args:
307 helper, cl = GetGerrit(opts, arg)
308 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400309
310
Mike Frysinger88f27292014-06-17 09:40:45 -0700311def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700312 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500313 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700314 # Allow for optional leading '~'.
315 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
316 add_list, remove_list, invalid_list = [], [], []
317
318 for x in emails:
319 if not email_validator.match(x):
320 invalid_list.append(x)
321 elif x[0] == '~':
322 remove_list.append(x[1:])
323 else:
324 add_list.append(x)
325
326 if invalid_list:
327 cros_build_lib.Die(
328 'Invalid email address(es): %s' % ', '.join(invalid_list))
329
330 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700331 helper, cl = GetGerrit(opts, cl)
332 helper.SetReviewers(cl, add=add_list, remove=remove_list,
333 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700334
335
Mike Frysinger88f27292014-06-17 09:40:45 -0700336def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530337 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700338 helper, cl = GetGerrit(opts, cl)
339 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530340
341
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800342def UserActTopic(opts, topic, *args):
343 """Set |topic| for CL number <n> [n ...]"""
344 for arg in args:
345 helper, arg = GetGerrit(opts, arg)
346 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
347
348
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700349def UserActDeletedraft(opts, *args):
350 """Delete draft patch set <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700351 for arg in args:
352 helper, cl = GetGerrit(opts, arg)
353 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800354
355
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800356def UserActAccount(opts):
357 """Get user account information."""
358 helper, _ = GetGerrit(opts)
359 pprint.PrettyPrinter().pprint(helper.GetAccount())
360
361
Mike Frysinger13f23a42013-05-13 17:32:01 -0400362def main(argv):
363 # Locate actions that are exposed to the user. All functions that start
364 # with "UserAct" are fair game.
365 act_pfx = 'UserAct'
366 actions = [x for x in globals() if x.startswith(act_pfx)]
367
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500368 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400369
370There is no support for doing line-by-line code review via the command line.
371This helps you manage various bits and CL status.
372
Mike Frysingera1db2c42014-06-15 00:42:48 -0700373For general Gerrit documentation, see:
374 https://gerrit-review.googlesource.com/Documentation/
375The Searching Changes page covers the search query syntax:
376 https://gerrit-review.googlesource.com/Documentation/user-search.html
377
Mike Frysinger13f23a42013-05-13 17:32:01 -0400378Example:
379 $ gerrit todo # List all the CLs that await your review.
380 $ gerrit mine # List all of your open CLs.
381 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
382 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
383 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700384Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700385 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
386ready.
387 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
388ready.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400389
390Actions:"""
391 indent = max([len(x) - len(act_pfx) for x in actions])
392 for a in sorted(actions):
Mike Frysinger15b23e42014-12-05 17:00:05 -0500393 cmd = a[len(act_pfx):]
394 # Sanity check for devs adding new commands. Should be quick.
395 if cmd != cmd.lower().capitalize():
396 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
397 (cmd, cmd.lower().capitalize()))
398 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400399
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500400 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500401 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Mike Frysinger88f27292014-06-17 09:40:45 -0700402 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysinger40541c62014-02-08 04:38:37 -0500403 const=constants.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500404 help='Query internal Chromium Gerrit instance')
405 parser.add_argument('-g', '--gob',
Mike Frysinger88f27292014-06-17 09:40:45 -0700406 default=constants.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500407 help=('Gerrit (on borg) instance to query (default: %s)' %
408 (constants.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500409 parser.add_argument('--sort', default='number',
410 help='Key to sort on (number, project)')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700411 parser.add_argument('--raw', default=False, action='store_true',
412 help='Return raw results (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700413 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
414 dest='dryrun',
415 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500416 parser.add_argument('-v', '--verbose', default=False, action='store_true',
417 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800418 parser.add_argument('-b', '--branch',
419 help='Limit output to the specific branch')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500420 parser.add_argument('args', nargs='+')
421 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400422
Mike Frysinger88f27292014-06-17 09:40:45 -0700423 # A cache of gerrit helpers we'll load on demand.
424 opts.gerrit = {}
425 opts.Freeze()
426
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400427 # pylint: disable=W0603
428 global COLOR
429 COLOR = terminal.Color(enabled=opts.color)
430
Mike Frysinger13f23a42013-05-13 17:32:01 -0400431 # Now look up the requested user action and run it.
Mike Frysinger88f27292014-06-17 09:40:45 -0700432 cmd = opts.args[0].lower()
433 args = opts.args[1:]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400434 functor = globals().get(act_pfx + cmd.capitalize())
435 if functor:
436 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700437 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700438 arg_min = getattr(functor, 'arg_min', len(argspec.args))
439 if len(args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700440 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700441 (cmd, arg_min))
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700442 elif len(argspec.args) - 1 != len(args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400443 parser.error('incorrect number of args: %s expects %s' %
444 (cmd, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700445 try:
446 functor(opts, *args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500447 except (cros_build_lib.RunCommandError, gerrit.GerritException,
448 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700449 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400450 else:
451 parser.error('unknown action: %s' % (cmd,))