blob: d68958577679b9b4f7fce3d1759ae1e24c969b39 [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
Mike Frysingerc85d8162014-02-08 00:45:21 -050026from chromite.lib import gob_util
Mike Frysinger10666292018-07-12 01:03:38 -040027from chromite.lib import memoize
Mike Frysinger13f23a42013-05-13 17:32:01 -040028from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040029from chromite.lib import uri_lib
Mike Frysinger13f23a42013-05-13 17:32:01 -040030
31
Mike Frysinger108eda22018-06-06 18:45:12 -040032# Locate actions that are exposed to the user. All functions that start
33# with "UserAct" are fair game.
34ACTION_PREFIX = 'UserAct'
35
36
Mike Frysinger031ad0b2013-05-14 18:15:34 -040037COLOR = None
Mike Frysinger13f23a42013-05-13 17:32:01 -040038
39# Map the internal names to the ones we normally show on the web ui.
40GERRIT_APPROVAL_MAP = {
Vadim Bendebury50571832013-11-12 10:43:19 -080041 'COMR': ['CQ', 'Commit Queue ',],
42 'CRVW': ['CR', 'Code Review ',],
43 'SUBM': ['S ', 'Submitted ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080044 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040045}
46
47# Order is important -- matches the web ui. This also controls the short
48# entries that we summarize in non-verbose mode.
49GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
50
51
52def red(s):
53 return COLOR.Color(terminal.Color.RED, s)
54
55
56def green(s):
57 return COLOR.Color(terminal.Color.GREEN, s)
58
59
60def blue(s):
61 return COLOR.Color(terminal.Color.BLUE, s)
62
63
64def limits(cls):
65 """Given a dict of fields, calculate the longest string lengths
66
67 This allows you to easily format the output of many results so that the
68 various cols all line up correctly.
69 """
70 lims = {}
71 for cl in cls:
72 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040073 # Use %s rather than str() to avoid codec issues.
74 # We also do this so we can format integers.
75 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040076 return lims
77
78
Mike Frysinger88f27292014-06-17 09:40:45 -070079# TODO: This func really needs to be merged into the core gerrit logic.
80def GetGerrit(opts, cl=None):
81 """Auto pick the right gerrit instance based on the |cl|
82
83 Args:
84 opts: The general options object.
85 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
86
87 Returns:
88 A tuple of a gerrit object and a sanitized CL #.
89 """
90 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -070091 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -060092 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -060093 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -060094 if cl.startswith('*'):
95 cl = cl[1:]
96 else:
97 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -070098 elif ':' in cl:
99 gob, cl = cl.split(':', 1)
100
101 if not gob in opts.gerrit:
102 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
103
104 return (opts.gerrit[gob], cl)
105
106
Mike Frysinger13f23a42013-05-13 17:32:01 -0400107def GetApprovalSummary(_opts, cls):
108 """Return a dict of the most important approvals"""
109 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700110 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
111 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
112 if not cats:
113 logging.warning('unknown gerrit approval type: %s', approver['type'])
114 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
120 elif approvs[cat] == '':
121 approvs[cat] = val
122 elif val < 0:
123 approvs[cat] = min(approvs[cat], val)
124 else:
125 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400126 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 Frysinger5f938ca2017-07-19 18:29:02 -0400181def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700182 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800183 if opts.branch is not None:
184 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800185 if opts.project is not None:
186 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800187 if opts.topic is not None:
188 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800189
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400190 if helper is None:
191 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700192 return helper.Query(query, raw=raw, bypass_cache=False)
193
194
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400195def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700196 """Query gerrit and filter/clean up the results"""
197 ret = []
198
Mike Frysinger2cd56022017-01-12 20:56:27 -0500199 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400200 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400201 # Gerrit likes to return a stats record too.
202 if not 'project' in cl:
203 continue
204
205 # Strip off common leading names since the result is still
206 # unique over the whole tree.
207 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400208 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
209 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400210 if cl['project'].startswith('%s/' % pfx):
211 cl['project'] = cl['project'][len(pfx) + 1:]
212
Mike Frysinger479f1192017-09-14 22:36:30 -0400213 cl['url'] = uri_lib.ShortenUri(cl['url'])
214
Mike Frysinger13f23a42013-05-13 17:32:01 -0400215 ret.append(cl)
216
Mike Frysingerb62313a2017-06-30 16:38:58 -0400217 if opts.sort == 'unsorted':
218 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700219 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400220 key = lambda x: int(x[opts.sort])
221 else:
222 key = lambda x: x[opts.sort]
223 return sorted(ret, key=key)
224
225
Mike Frysinger13f23a42013-05-13 17:32:01 -0400226def UserActTodo(opts):
227 """List CLs needing your review"""
Mike Frysinger87690552018-12-30 22:56:06 -0500228 cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
Mike Frysingered3d7ea2017-07-10 13:14:02 -0400229 'label:Code-Review=0,user=self '
230 'NOT label:Verified<0'))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400231 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400232
233
Mike Frysingera1db2c42014-06-15 00:42:48 -0700234def UserActSearch(opts, query):
Harry Cutts26076b32019-02-26 15:01:29 -0800235 """List CLs matching the search query"""
Mike Frysingera1db2c42014-06-15 00:42:48 -0700236 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400237 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800238UserActSearch.usage = '<query>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400239
240
Mike Frysingera1db2c42014-06-15 00:42:48 -0700241def UserActMine(opts):
242 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700243 if opts.draft:
244 rule = 'is:draft'
245 else:
246 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500247 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700248
249
Paul Hobbs89765232015-06-24 14:07:49 -0700250def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
251 """Runs breadth first search starting from the nodes in |to_visit|
252
253 Args:
254 to_visit: the starting nodes
255 children: a function which takes a node and returns the nodes adjacent to it
256 visited_key: a function for deduplicating node visits. Defaults to the
257 identity function (lambda x: x)
258
259 Returns:
260 A list of nodes which are reachable from any node in |to_visit| by calling
261 |children| any number of times.
262 """
263 to_visit = list(to_visit)
264 seen = set(map(visited_key, to_visit))
265 for node in to_visit:
266 for child in children(node):
267 key = visited_key(child)
268 if key not in seen:
269 seen.add(key)
270 to_visit.append(child)
271 return to_visit
272
273
274def UserActDeps(opts, query):
275 """List CLs matching a query, and all transitive dependencies of those CLs"""
276 cls = _Query(opts, query, raw=False)
277
Mike Frysinger10666292018-07-12 01:03:38 -0400278 @memoize.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400279 def _QueryChange(cl, helper=None):
280 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700281
Mike Frysinger5726da92017-09-20 22:14:25 -0400282 def _ProcessDeps(cl, deps, required):
283 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700284 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400285 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400286 if not dep.remote in opts.gerrit:
287 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
288 remote=dep.remote, print_cmd=opts.debug)
289 helper = opts.gerrit[dep.remote]
290
Paul Hobbs89765232015-06-24 14:07:49 -0700291 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400292 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
293
294 # Handle empty results. If we found a commit that was pushed directly
295 # (e.g. a bot commit), then gerrit won't know about it.
296 if not changes:
297 if required:
298 logging.error('CL %s depends on %s which cannot be found',
299 cl, dep.ToGerritQueryText())
300 continue
301
302 # Our query might have matched more than one result. This can come up
303 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
304 # across multiple repos/branches. We blindly check all of them in the
305 # hopes that all open ones are what the user wants, but then again the
306 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
307 if len(changes) > 1:
308 logging.warning('CL %s has an ambiguous CQ dependency %s',
309 cl, dep.ToGerritQueryText())
310 for change in changes:
311 if change.status == 'NEW':
312 yield change
313
314 def _Children(cl):
315 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
316 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
317 yield change
318 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
319 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700320
321 transitives = _BreadthFirstSearch(
322 cls, _Children,
323 visited_key=lambda cl: cl.gerrit_number)
324
325 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400326 PrintCls(opts, transitives_raw)
Harry Cutts26076b32019-02-26 15:01:29 -0800327UserActDeps.usage = '<query>'
Paul Hobbs89765232015-06-24 14:07:49 -0700328
329
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700330def UserActInspect(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800331 """Show the details of one or more CLs"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400332 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700333 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400334 helper, cl = GetGerrit(opts, arg)
335 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
336 if change:
337 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700338 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400339 logging.warning('no results found for CL %s', arg)
340 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800341UserActInspect.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400342
343
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700344def UserActReview(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800345 """Mark CLs with a code review status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700346 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700347 for arg in args[:-1]:
348 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800349 helper.SetReview(cl, labels={'Code-Review': num},
350 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700351UserActReview.arg_min = 2
Harry Cutts26076b32019-02-26 15:01:29 -0800352UserActReview.usage = '<CLs...> <-2|-1|0|1|2>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400353
354
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700355def UserActVerify(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800356 """Mark CLs with a verified status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700357 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700358 for arg in args[:-1]:
359 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800360 helper.SetReview(cl, labels={'Verified': num},
361 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700362UserActVerify.arg_min = 2
Harry Cutts26076b32019-02-26 15:01:29 -0800363UserActVerify.usage = '<CLs...> <-1|0|1>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400364
365
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700366def UserActReady(opts, *args):
Jason D. Clinton88aed612019-04-07 20:24:05 -0600367 """Mark CLs with CQ dryrun (1) or ready (2) status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700368 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700369 for arg in args[:-1]:
370 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800371 helper.SetReview(cl, labels={'Commit-Queue': num},
372 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700373UserActReady.arg_min = 2
Jason D. Clinton88aed612019-04-07 20:24:05 -0600374UserActReady.usage = '<CLs...> <0|1|2>'
Mike Frysinger15b23e42014-12-05 17:00:05 -0500375
376
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700377def UserActSubmit(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800378 """Submit CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700379 for arg in args:
380 helper, cl = GetGerrit(opts, arg)
381 helper.SubmitChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800382UserActSubmit.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400383
384
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700385def UserActAbandon(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800386 """Abandon CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700387 for arg in args:
388 helper, cl = GetGerrit(opts, arg)
389 helper.AbandonChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800390UserActAbandon.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400391
392
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700393def UserActRestore(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800394 """Restore CLs that were abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700395 for arg in args:
396 helper, cl = GetGerrit(opts, arg)
397 helper.RestoreChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800398UserActRestore.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400399
400
Mike Frysinger88f27292014-06-17 09:40:45 -0700401def UserActReviewers(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800402 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500403 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700404 # Allow for optional leading '~'.
405 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
406 add_list, remove_list, invalid_list = [], [], []
407
408 for x in emails:
409 if not email_validator.match(x):
410 invalid_list.append(x)
411 elif x[0] == '~':
412 remove_list.append(x[1:])
413 else:
414 add_list.append(x)
415
416 if invalid_list:
417 cros_build_lib.Die(
418 'Invalid email address(es): %s' % ', '.join(invalid_list))
419
420 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700421 helper, cl = GetGerrit(opts, cl)
422 helper.SetReviewers(cl, add=add_list, remove=remove_list,
423 dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800424UserActReviewers.usage = '<CL> <emails...>'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700425
426
Allen Li38abdaa2017-03-16 13:25:02 -0700427def UserActAssign(opts, cl, assignee):
Harry Cutts26076b32019-02-26 15:01:29 -0800428 """Set the assignee for a CL"""
Allen Li38abdaa2017-03-16 13:25:02 -0700429 helper, cl = GetGerrit(opts, cl)
430 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800431UserActAssign.usage = '<CL> <assignee>'
Allen Li38abdaa2017-03-16 13:25:02 -0700432
433
Mike Frysinger88f27292014-06-17 09:40:45 -0700434def UserActMessage(opts, cl, message):
Harry Cutts26076b32019-02-26 15:01:29 -0800435 """Add a message to a CL"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700436 helper, cl = GetGerrit(opts, cl)
437 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800438UserActMessage.usage = '<CL> <message>'
Doug Anderson8119df02013-07-20 21:00:24 +0530439
440
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800441def UserActTopic(opts, topic, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800442 """Set a topic for one or more CLs"""
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800443 for arg in args:
444 helper, arg = GetGerrit(opts, arg)
445 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800446UserActTopic.usage = '<topic> <CLs...>'
447
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800448
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700449def UserActPrivate(opts, cl, private_str):
Harry Cutts26076b32019-02-26 15:01:29 -0800450 """Set the private bit on a CL to private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700451 try:
452 private = cros_build_lib.BooleanShellValue(private_str, False)
453 except ValueError:
454 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
455
456 helper, cl = GetGerrit(opts, cl)
457 helper.SetPrivate(cl, private)
Harry Cutts26076b32019-02-26 15:01:29 -0800458UserActPrivate.usage = '<CL> <private str>'
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700459
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800460
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800461def UserActSethashtags(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800462 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800463 hashtags = args
464 add = []
465 remove = []
466 for hashtag in hashtags:
467 if hashtag.startswith('~'):
468 remove.append(hashtag[1:])
469 else:
470 add.append(hashtag)
471 helper, cl = GetGerrit(opts, cl)
472 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800473UserActSethashtags.usage = '<CL> <hashtags...>'
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800474
475
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700476def UserActDeletedraft(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800477 """Delete draft CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700478 for arg in args:
479 helper, cl = GetGerrit(opts, arg)
480 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800481UserActDeletedraft.usage = '<CLs...>'
Jon Salza427fb02014-03-07 18:13:17 +0800482
483
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800484def UserActAccount(opts):
Harry Cutts26076b32019-02-26 15:01:29 -0800485 """Get the current user account information"""
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800486 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400487 acct = helper.GetAccount()
488 if opts.json:
489 json.dump(acct, sys.stdout)
490 else:
491 print('account_id:%i %s <%s>' %
492 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800493
494
Harry Cutts26076b32019-02-26 15:01:29 -0800495def _GetActionUsages():
496 """Formats a one-line usage and doc message for each action."""
497 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
498 actions.sort()
499
500 cmds = [x[len(ACTION_PREFIX):] for x in actions]
501
502 # Sanity check names for devs adding new commands. Should be quick.
503 for cmd in cmds:
504 expected_name = cmd.lower().capitalize()
505 if cmd != expected_name:
506 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
507 (cmd, expected_name))
508
509 functions = [globals()[x] for x in actions]
510 usages = [getattr(x, 'usage', '') for x in functions]
511 docs = [x.__doc__ for x in functions]
512
513 action_usages = []
514 cmd_indent = len(max(cmds, key=len))
515 usage_indent = len(max(usages, key=len))
516 for cmd, usage, doc in zip(cmds, usages, docs):
517 action_usages.append(' %-*s %-*s : %s' %
518 (cmd_indent, cmd.lower(), usage_indent, usage, doc))
519
520 return '\n'.join(action_usages)
521
522
Mike Frysinger108eda22018-06-06 18:45:12 -0400523def GetParser():
524 """Returns the parser to use for this module."""
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500525 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400526
527There is no support for doing line-by-line code review via the command line.
528This helps you manage various bits and CL status.
529
Mike Frysingera1db2c42014-06-15 00:42:48 -0700530For general Gerrit documentation, see:
531 https://gerrit-review.googlesource.com/Documentation/
532The Searching Changes page covers the search query syntax:
533 https://gerrit-review.googlesource.com/Documentation/user-search.html
534
Mike Frysinger13f23a42013-05-13 17:32:01 -0400535Example:
536 $ gerrit todo # List all the CLs that await your review.
537 $ gerrit mine # List all of your open CLs.
538 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
539 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
540 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700541Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700542 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
543ready.
544 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
545ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400546 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400547
Harry Cutts26076b32019-02-26 15:01:29 -0800548Actions:
549"""
550 usage += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400551
Alex Klein2ab29cc2018-07-19 12:01:00 -0600552 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500553 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500554 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600555 default=site_params.EXTERNAL_GOB_INSTANCE,
556 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500557 help='Query internal Chromium Gerrit instance')
558 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600559 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500560 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600561 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500562 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400563 help='Key to sort on (number, project); use "unsorted" '
564 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700565 parser.add_argument('--raw', default=False, action='store_true',
566 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400567 parser.add_argument('--json', default=False, action='store_true',
568 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700569 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
570 dest='dryrun',
571 help='Show what would be done, but do not make changes')
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800572 parser.add_argument('--ne', '--no-emails', default=True, action='store_false',
573 dest='send_email',
574 help='Do not send email for some operations '
575 '(e.g. ready/review/trybotready/verify)')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500576 parser.add_argument('-v', '--verbose', default=False, action='store_true',
577 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800578 parser.add_argument('-b', '--branch',
579 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700580 parser.add_argument('--draft', default=False, action='store_true',
581 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800582 parser.add_argument('-p', '--project',
583 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800584 parser.add_argument('-t', '--topic',
585 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500586 parser.add_argument('action', help='The gerrit action to perform')
587 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400588
589 return parser
590
591
592def main(argv):
593 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500594 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400595
Mike Frysinger88f27292014-06-17 09:40:45 -0700596 # A cache of gerrit helpers we'll load on demand.
597 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800598
599 # Convert user friendly command line option into a gerrit parameter.
600 opts.notify = 'ALL' if opts.send_email else 'NONE'
Mike Frysinger88f27292014-06-17 09:40:45 -0700601 opts.Freeze()
602
Mike Frysinger27e21b72018-07-12 14:20:21 -0400603 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400604 global COLOR
605 COLOR = terminal.Color(enabled=opts.color)
606
Mike Frysinger13f23a42013-05-13 17:32:01 -0400607 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400608 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400609 if functor:
610 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700611 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700612 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500613 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700614 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500615 (opts.action, arg_min))
616 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400617 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500618 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700619 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500620 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500621 except (cros_build_lib.RunCommandError, gerrit.GerritException,
622 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700623 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400624 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500625 parser.error('unknown action: %s' % (opts.action,))