blob: b859e4064bab202d9b288c47aefed44a05f12f3a [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Mike Frysinger13f23a42013-05-13 17:32:01 -04002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysinger08737512014-02-07 22:58:26 -05006"""A command line interface to Gerrit-on-borg instances.
Mike Frysinger13f23a42013-05-13 17:32:01 -04007
8Internal Note:
9To expose a function directly to the command line interface, name your function
10with the prefix "UserAct".
11"""
12
Mike Frysinger31ff6f92014-02-08 04:33:03 -050013from __future__ import print_function
14
Mike Frysinger13f23a42013-05-13 17:32:01 -040015import inspect
Mike Frysinger87c74ce2017-04-04 16:12:31 -040016import json
Vadim Bendeburydcfe2322013-05-23 10:54:49 -070017import re
Mike Frysinger87c74ce2017-04-04 16:12:31 -040018import sys
Mike Frysinger13f23a42013-05-13 17:32:01 -040019
Aviv Keshetb7519e12016-10-04 00:50:00 -070020from chromite.lib import config_lib
21from chromite.lib import constants
Mike Frysinger13f23a42013-05-13 17:32:01 -040022from chromite.lib import commandline
23from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070024from chromite.lib import cros_logging as logging
Mike Frysinger13f23a42013-05-13 17:32:01 -040025from chromite.lib import gerrit
Mathieu Olivari04b4d522014-12-18 17:26:34 -080026from chromite.lib import git
Mike Frysingerc85d8162014-02-08 00:45:21 -050027from chromite.lib import gob_util
Mike Frysinger10666292018-07-12 01:03:38 -040028from chromite.lib import memoize
Mike Frysinger13f23a42013-05-13 17:32:01 -040029from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040030from chromite.lib import uri_lib
Mike Frysinger13f23a42013-05-13 17:32:01 -040031
32
Mike Frysinger108eda22018-06-06 18:45:12 -040033# Locate actions that are exposed to the user. All functions that start
34# with "UserAct" are fair game.
35ACTION_PREFIX = 'UserAct'
36
37
Mike Frysinger031ad0b2013-05-14 18:15:34 -040038COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040039
40# Map the internal names to the ones we normally show on the web ui.
41GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080042 'COMR': ['CQ', 'Commit Queue ',],
43 'CRVW': ['CR', 'Code Review ',],
44 'SUBM': ['S ', 'Submitted ',],
David James2b2e2c52014-12-02 19:32:07 -080045 'TRY': ['T ', 'Trybot Ready ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080046 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040047}
48
49# Order is important -- matches the web ui. This also controls the short
50# entries that we summarize in non-verbose mode.
51GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
52
53
54def red(s):
55 return COLOR.Color(terminal.Color.RED, s)
56
57
58def green(s):
59 return COLOR.Color(terminal.Color.GREEN, s)
60
61
62def blue(s):
63 return COLOR.Color(terminal.Color.BLUE, s)
64
65
66def limits(cls):
67 """Given a dict of fields, calculate the longest string lengths
68
69 This allows you to easily format the output of many results so that the
70 various cols all line up correctly.
71 """
72 lims = {}
73 for cl in cls:
74 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040075 # Use %s rather than str() to avoid codec issues.
76 # We also do this so we can format integers.
77 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040078 return lims
79
80
Mike Frysinger88f27292014-06-17 09:40:45 -070081# TODO: This func really needs to be merged into the core gerrit logic.
82def GetGerrit(opts, cl=None):
83 """Auto pick the right gerrit instance based on the |cl|
84
85 Args:
86 opts: The general options object.
87 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
88
89 Returns:
90 A tuple of a gerrit object and a sanitized CL #.
91 """
92 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -070093 if cl is not None:
Mike Frysinger88f27292014-06-17 09:40:45 -070094 if cl.startswith('*'):
Alex Klein2ab29cc2018-07-19 12:01:00 -060095 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Mike Frysinger88f27292014-06-17 09:40:45 -070096 cl = cl[1:]
97 elif ':' in cl:
98 gob, cl = cl.split(':', 1)
99
100 if not gob in opts.gerrit:
101 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
102
103 return (opts.gerrit[gob], cl)
104
105
Mike Frysinger13f23a42013-05-13 17:32:01 -0400106def GetApprovalSummary(_opts, cls):
107 """Return a dict of the most important approvals"""
108 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
109 if 'approvals' in cls['currentPatchSet']:
110 for approver in cls['currentPatchSet']['approvals']:
111 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
112 if not cats:
Ralph Nathan446aee92015-03-23 14:44:56 -0700113 logging.warning('unknown gerrit approval type: %s', approver['type'])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400114 continue
115 cat = cats[0].strip()
116 val = int(approver['value'])
117 if not cat in approvs:
118 # Ignore the extended categories in the summary view.
119 continue
Mike Frysingera0313d02017-07-10 16:44:43 -0400120 elif approvs[cat] == '':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400121 approvs[cat] = val
122 elif val < 0:
123 approvs[cat] = min(approvs[cat], val)
124 else:
125 approvs[cat] = max(approvs[cat], val)
126 return approvs
127
128
Mike Frysingera1b4b272017-04-05 16:11:00 -0400129def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400130 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400131 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400132 lims = {'url': 0, 'project': 0}
133
134 status = ''
135 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400136 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400137 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400138 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400139 functor = lambda x: x
140 elif approvs[cat] < 0:
141 functor = red
142 else:
143 functor = green
144 status += functor('%s:%2s ' % (cat, approvs[cat]))
145
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400146 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
147 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400148
149 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400150 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400151 functor = red if int(approver['value']) < 0 else green
152 n = functor('%2s' % approver['value'])
153 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
154 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500155 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400156
157
Mike Frysingera1b4b272017-04-05 16:11:00 -0400158def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400159 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400160 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600161 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400162 pfx = ''
163 # Special case internal Chrome GoB as that is what most devs use.
164 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600165 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
166 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400167 for cl in cls:
168 print('%s%s' % (pfx, cl['number']))
169
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400170 elif opts.json:
171 json.dump(cls, sys.stdout)
172
Mike Frysingera1b4b272017-04-05 16:11:00 -0400173 else:
174 if lims is None:
175 lims = limits(cls)
176
177 for cl in cls:
178 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
179
180
Mike Frysinger13f23a42013-05-13 17:32:01 -0400181def _MyUserInfo():
Mike Frysinger2cd56022017-01-12 20:56:27 -0500182 """Try to return e-mail addresses used by the active user."""
183 return [git.GetProjectUserEmail(constants.CHROMITE_DIR)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400184
185
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400186def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700187 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800188 if opts.branch is not None:
189 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800190 if opts.project is not None:
191 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800192 if opts.topic is not None:
193 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800194
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400195 if helper is None:
196 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700197 return helper.Query(query, raw=raw, bypass_cache=False)
198
199
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400200def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700201 """Query gerrit and filter/clean up the results"""
202 ret = []
203
Mike Frysinger2cd56022017-01-12 20:56:27 -0500204 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400205 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400206 # Gerrit likes to return a stats record too.
207 if not 'project' in cl:
208 continue
209
210 # Strip off common leading names since the result is still
211 # unique over the whole tree.
212 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400213 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
214 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400215 if cl['project'].startswith('%s/' % pfx):
216 cl['project'] = cl['project'][len(pfx) + 1:]
217
Mike Frysinger479f1192017-09-14 22:36:30 -0400218 cl['url'] = uri_lib.ShortenUri(cl['url'])
219
Mike Frysinger13f23a42013-05-13 17:32:01 -0400220 ret.append(cl)
221
Mike Frysingerb62313a2017-06-30 16:38:58 -0400222 if opts.sort == 'unsorted':
223 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700224 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400225 key = lambda x: int(x[opts.sort])
226 else:
227 key = lambda x: x[opts.sort]
228 return sorted(ret, key=key)
229
230
Mike Frysinger13f23a42013-05-13 17:32:01 -0400231def IsApprover(cl, users):
232 """See if the approvers in |cl| is listed in |users|"""
233 # See if we are listed in the approvals list. We have to parse
234 # this by hand as the gerrit query system doesn't support it :(
235 # http://code.google.com/p/gerrit/issues/detail?id=1235
236 if 'approvals' not in cl['currentPatchSet']:
237 return False
238
239 if isinstance(users, basestring):
240 users = (users,)
241
242 for approver in cl['currentPatchSet']['approvals']:
Stefan Zager29560302013-09-06 14:30:54 -0700243 if (approver['by']['email'] in users and
244 approver['type'] == 'CRVW' and
245 int(approver['value']) != 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400246 return True
247
248 return False
249
250
251def UserActTodo(opts):
252 """List CLs needing your review"""
Mike Frysinger2cd56022017-01-12 20:56:27 -0500253 emails = _MyUserInfo()
254 cls = FilteredQuery(opts, 'reviewer:self status:open NOT owner:self')
Mike Frysinger13f23a42013-05-13 17:32:01 -0400255 cls = [x for x in cls if not IsApprover(x, emails)]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400256 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400257
258
Mike Frysingera1db2c42014-06-15 00:42:48 -0700259def UserActSearch(opts, query):
260 """List CLs matching the Gerrit <search query>"""
261 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400262 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400263
264
Mike Frysingera1db2c42014-06-15 00:42:48 -0700265def UserActMine(opts):
266 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700267 if opts.draft:
268 rule = 'is:draft'
269 else:
270 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500271 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700272
273
Paul Hobbs89765232015-06-24 14:07:49 -0700274def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
275 """Runs breadth first search starting from the nodes in |to_visit|
276
277 Args:
278 to_visit: the starting nodes
279 children: a function which takes a node and returns the nodes adjacent to it
280 visited_key: a function for deduplicating node visits. Defaults to the
281 identity function (lambda x: x)
282
283 Returns:
284 A list of nodes which are reachable from any node in |to_visit| by calling
285 |children| any number of times.
286 """
287 to_visit = list(to_visit)
288 seen = set(map(visited_key, to_visit))
289 for node in to_visit:
290 for child in children(node):
291 key = visited_key(child)
292 if key not in seen:
293 seen.add(key)
294 to_visit.append(child)
295 return to_visit
296
297
298def UserActDeps(opts, query):
299 """List CLs matching a query, and all transitive dependencies of those CLs"""
300 cls = _Query(opts, query, raw=False)
301
Mike Frysinger10666292018-07-12 01:03:38 -0400302 @memoize.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400303 def _QueryChange(cl, helper=None):
304 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700305
Mike Frysinger5726da92017-09-20 22:14:25 -0400306 def _ProcessDeps(cl, deps, required):
307 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700308 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400309 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400310 if not dep.remote in opts.gerrit:
311 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
312 remote=dep.remote, print_cmd=opts.debug)
313 helper = opts.gerrit[dep.remote]
314
Paul Hobbs89765232015-06-24 14:07:49 -0700315 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400316 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
317
318 # Handle empty results. If we found a commit that was pushed directly
319 # (e.g. a bot commit), then gerrit won't know about it.
320 if not changes:
321 if required:
322 logging.error('CL %s depends on %s which cannot be found',
323 cl, dep.ToGerritQueryText())
324 continue
325
326 # Our query might have matched more than one result. This can come up
327 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
328 # across multiple repos/branches. We blindly check all of them in the
329 # hopes that all open ones are what the user wants, but then again the
330 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
331 if len(changes) > 1:
332 logging.warning('CL %s has an ambiguous CQ dependency %s',
333 cl, dep.ToGerritQueryText())
334 for change in changes:
335 if change.status == 'NEW':
336 yield change
337
338 def _Children(cl):
339 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
340 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
341 yield change
342 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
343 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700344
345 transitives = _BreadthFirstSearch(
346 cls, _Children,
347 visited_key=lambda cl: cl.gerrit_number)
348
349 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400350 PrintCls(opts, transitives_raw)
Paul Hobbs89765232015-06-24 14:07:49 -0700351
352
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700353def UserActInspect(opts, *args):
354 """Inspect CL number <n> [n ...]"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400355 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700356 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400357 helper, cl = GetGerrit(opts, arg)
358 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
359 if change:
360 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700361 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400362 logging.warning('no results found for CL %s', arg)
363 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400364
365
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700366def UserActReview(opts, *args):
367 """Mark CL <n> [n ...] with code review status <-2,-1,0,1,2>"""
368 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700369 for arg in args[:-1]:
370 helper, cl = GetGerrit(opts, arg)
371 helper.SetReview(cl, labels={'Code-Review': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700372UserActReview.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400373
374
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700375def UserActVerify(opts, *args):
376 """Mark CL <n> [n ...] with verify status <-1,0,1>"""
377 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700378 for arg in args[:-1]:
379 helper, cl = GetGerrit(opts, arg)
380 helper.SetReview(cl, labels={'Verified': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700381UserActVerify.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400382
383
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700384def UserActReady(opts, *args):
Kirtika Ruchandanica852f42017-05-23 18:18:05 -0700385 """Mark CL <n> [n ...] with ready status <0,1>"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700386 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700387 for arg in args[:-1]:
388 helper, cl = GetGerrit(opts, arg)
389 helper.SetReview(cl, labels={'Commit-Queue': num}, dryrun=opts.dryrun)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700390UserActReady.arg_min = 2
Mike Frysinger13f23a42013-05-13 17:32:01 -0400391
392
Mike Frysinger15b23e42014-12-05 17:00:05 -0500393def UserActTrybotready(opts, *args):
394 """Mark CL <n> [n ...] with trybot-ready status <0,1>"""
395 num = args[-1]
396 for arg in args[:-1]:
397 helper, cl = GetGerrit(opts, arg)
398 helper.SetReview(cl, labels={'Trybot-Ready': num}, dryrun=opts.dryrun)
399UserActTrybotready.arg_min = 2
400
401
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700402def UserActSubmit(opts, *args):
403 """Submit CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700404 for arg in args:
405 helper, cl = GetGerrit(opts, arg)
406 helper.SubmitChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400407
408
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700409def UserActAbandon(opts, *args):
410 """Abandon CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700411 for arg in args:
412 helper, cl = GetGerrit(opts, arg)
413 helper.AbandonChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400414
415
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700416def UserActRestore(opts, *args):
417 """Restore CL <n> [n ...] that was abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700418 for arg in args:
419 helper, cl = GetGerrit(opts, arg)
420 helper.RestoreChange(cl, dryrun=opts.dryrun)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400421
422
Mike Frysinger88f27292014-06-17 09:40:45 -0700423def UserActReviewers(opts, cl, *args):
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700424 """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500425 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700426 # Allow for optional leading '~'.
427 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
428 add_list, remove_list, invalid_list = [], [], []
429
430 for x in emails:
431 if not email_validator.match(x):
432 invalid_list.append(x)
433 elif x[0] == '~':
434 remove_list.append(x[1:])
435 else:
436 add_list.append(x)
437
438 if invalid_list:
439 cros_build_lib.Die(
440 'Invalid email address(es): %s' % ', '.join(invalid_list))
441
442 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700443 helper, cl = GetGerrit(opts, cl)
444 helper.SetReviewers(cl, add=add_list, remove=remove_list,
445 dryrun=opts.dryrun)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700446
447
Allen Li38abdaa2017-03-16 13:25:02 -0700448def UserActAssign(opts, cl, assignee):
449 """Set assignee for CL <n>"""
450 helper, cl = GetGerrit(opts, cl)
451 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
452
453
Mike Frysinger88f27292014-06-17 09:40:45 -0700454def UserActMessage(opts, cl, message):
Doug Anderson8119df02013-07-20 21:00:24 +0530455 """Add a message to CL <n>"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700456 helper, cl = GetGerrit(opts, cl)
457 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Doug Anderson8119df02013-07-20 21:00:24 +0530458
459
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800460def UserActTopic(opts, topic, *args):
461 """Set |topic| for CL number <n> [n ...]"""
462 for arg in args:
463 helper, arg = GetGerrit(opts, arg)
464 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
465
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700466def UserActPrivate(opts, cl, private_str):
467 """Set private bit on CL to private"""
468 try:
469 private = cros_build_lib.BooleanShellValue(private_str, False)
470 except ValueError:
471 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
472
473 helper, cl = GetGerrit(opts, cl)
474 helper.SetPrivate(cl, private)
475
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800476
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800477def UserActSethashtags(opts, cl, *args):
478 """Add/remove hashtags for CL <n> (prepend with '~' to remove)"""
479 hashtags = args
480 add = []
481 remove = []
482 for hashtag in hashtags:
483 if hashtag.startswith('~'):
484 remove.append(hashtag[1:])
485 else:
486 add.append(hashtag)
487 helper, cl = GetGerrit(opts, cl)
488 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
489
490
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700491def UserActDeletedraft(opts, *args):
Marc Herbert02448c82015-10-07 14:03:34 -0700492 """Delete draft CL <n> [n ...]"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700493 for arg in args:
494 helper, cl = GetGerrit(opts, arg)
495 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Jon Salza427fb02014-03-07 18:13:17 +0800496
497
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800498def UserActAccount(opts):
499 """Get user account information."""
500 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400501 acct = helper.GetAccount()
502 if opts.json:
503 json.dump(acct, sys.stdout)
504 else:
505 print('account_id:%i %s <%s>' %
506 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800507
508
Mike Frysinger108eda22018-06-06 18:45:12 -0400509def GetParser():
510 """Returns the parser to use for this module."""
511 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400512
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500513 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400514
515There is no support for doing line-by-line code review via the command line.
516This helps you manage various bits and CL status.
517
Mike Frysingera1db2c42014-06-15 00:42:48 -0700518For general Gerrit documentation, see:
519 https://gerrit-review.googlesource.com/Documentation/
520The Searching Changes page covers the search query syntax:
521 https://gerrit-review.googlesource.com/Documentation/user-search.html
522
Mike Frysinger13f23a42013-05-13 17:32:01 -0400523Example:
524 $ gerrit todo # List all the CLs that await your review.
525 $ gerrit mine # List all of your open CLs.
526 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
527 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
528 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700529Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700530 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
531ready.
532 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
533ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400534 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400535
536Actions:"""
Mike Frysinger108eda22018-06-06 18:45:12 -0400537 indent = max([len(x) - len(ACTION_PREFIX) for x in actions])
Mike Frysinger13f23a42013-05-13 17:32:01 -0400538 for a in sorted(actions):
Mike Frysinger108eda22018-06-06 18:45:12 -0400539 cmd = a[len(ACTION_PREFIX):]
Mike Frysinger15b23e42014-12-05 17:00:05 -0500540 # Sanity check for devs adding new commands. Should be quick.
541 if cmd != cmd.lower().capitalize():
542 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
543 (cmd, cmd.lower().capitalize()))
544 usage += '\n %-*s: %s' % (indent, cmd.lower(), globals()[a].__doc__)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400545
Alex Klein2ab29cc2018-07-19 12:01:00 -0600546 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500547 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500548 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600549 default=site_params.EXTERNAL_GOB_INSTANCE,
550 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500551 help='Query internal Chromium Gerrit instance')
552 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600553 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500554 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600555 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500556 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400557 help='Key to sort on (number, project); use "unsorted" '
558 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700559 parser.add_argument('--raw', default=False, action='store_true',
560 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400561 parser.add_argument('--json', default=False, action='store_true',
562 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700563 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
564 dest='dryrun',
565 help='Show what would be done, but do not make changes')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500566 parser.add_argument('-v', '--verbose', default=False, action='store_true',
567 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800568 parser.add_argument('-b', '--branch',
569 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700570 parser.add_argument('--draft', default=False, action='store_true',
571 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800572 parser.add_argument('-p', '--project',
573 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800574 parser.add_argument('-t', '--topic',
575 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500576 parser.add_argument('action', help='The gerrit action to perform')
577 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400578
579 return parser
580
581
582def main(argv):
583 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500584 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400585
Mike Frysinger88f27292014-06-17 09:40:45 -0700586 # A cache of gerrit helpers we'll load on demand.
587 opts.gerrit = {}
588 opts.Freeze()
589
Mike Frysinger27e21b72018-07-12 14:20:21 -0400590 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400591 global COLOR
592 COLOR = terminal.Color(enabled=opts.color)
593
Mike Frysinger13f23a42013-05-13 17:32:01 -0400594 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400595 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400596 if functor:
597 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700598 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700599 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500600 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700601 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500602 (opts.action, arg_min))
603 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400604 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500605 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700606 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500607 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500608 except (cros_build_lib.RunCommandError, gerrit.GerritException,
609 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700610 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400611 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500612 parser.error('unknown action: %s' % (opts.action,))