blob: 778e55e2fb3a99551b64054aeb41f61c921b8059 [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 ',],
David James2b2e2c52014-12-02 19:32:07 -080044 'TRY': ['T ', 'Trybot Ready ',],
Vadim Bendebury50571832013-11-12 10:43:19 -080045 'VRIF': ['V ', 'Verified ',],
Mike Frysinger13f23a42013-05-13 17:32:01 -040046}
47
48# Order is important -- matches the web ui. This also controls the short
49# entries that we summarize in non-verbose mode.
50GERRIT_SUMMARY_CATS = ('CR', 'CQ', 'V',)
51
52
53def red(s):
54 return COLOR.Color(terminal.Color.RED, s)
55
56
57def green(s):
58 return COLOR.Color(terminal.Color.GREEN, s)
59
60
61def blue(s):
62 return COLOR.Color(terminal.Color.BLUE, s)
63
64
65def limits(cls):
66 """Given a dict of fields, calculate the longest string lengths
67
68 This allows you to easily format the output of many results so that the
69 various cols all line up correctly.
70 """
71 lims = {}
72 for cl in cls:
73 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040074 # Use %s rather than str() to avoid codec issues.
75 # We also do this so we can format integers.
76 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040077 return lims
78
79
Mike Frysinger88f27292014-06-17 09:40:45 -070080# TODO: This func really needs to be merged into the core gerrit logic.
81def GetGerrit(opts, cl=None):
82 """Auto pick the right gerrit instance based on the |cl|
83
84 Args:
85 opts: The general options object.
86 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
87
88 Returns:
89 A tuple of a gerrit object and a sanitized CL #.
90 """
91 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -070092 if cl is not None:
Mike Frysinger88f27292014-06-17 09:40:45 -070093 if cl.startswith('*'):
Alex Klein2ab29cc2018-07-19 12:01:00 -060094 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Mike Frysinger88f27292014-06-17 09:40:45 -070095 cl = cl[1:]
96 elif ':' in cl:
97 gob, cl = cl.split(':', 1)
98
99 if not gob in opts.gerrit:
100 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
101
102 return (opts.gerrit[gob], cl)
103
104
Mike Frysinger13f23a42013-05-13 17:32:01 -0400105def GetApprovalSummary(_opts, cls):
106 """Return a dict of the most important approvals"""
107 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700108 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
109 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
110 if not cats:
111 logging.warning('unknown gerrit approval type: %s', approver['type'])
112 continue
113 cat = cats[0].strip()
114 val = int(approver['value'])
115 if not cat in approvs:
116 # Ignore the extended categories in the summary view.
117 continue
118 elif approvs[cat] == '':
119 approvs[cat] = val
120 elif val < 0:
121 approvs[cat] = min(approvs[cat], val)
122 else:
123 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400124 return approvs
125
126
Mike Frysingera1b4b272017-04-05 16:11:00 -0400127def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400128 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400129 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400130 lims = {'url': 0, 'project': 0}
131
132 status = ''
133 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400134 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400135 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400136 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400137 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 Frysingerb4a3e3c2017-04-05 16:06:53 -0400144 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
145 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400146
147 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400148 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400149 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
Mike Frysingera1b4b272017-04-05 16:11:00 -0400156def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400157 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400158 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600159 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400160 pfx = ''
161 # Special case internal Chrome GoB as that is what most devs use.
162 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600163 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
164 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400165 for cl in cls:
166 print('%s%s' % (pfx, cl['number']))
167
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400168 elif opts.json:
169 json.dump(cls, sys.stdout)
170
Mike Frysingera1b4b272017-04-05 16:11:00 -0400171 else:
172 if lims is None:
173 lims = limits(cls)
174
175 for cl in cls:
176 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
177
178
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400179def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700180 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800181 if opts.branch is not None:
182 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800183 if opts.project is not None:
184 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800185 if opts.topic is not None:
186 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800187
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400188 if helper is None:
189 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700190 return helper.Query(query, raw=raw, bypass_cache=False)
191
192
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400193def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700194 """Query gerrit and filter/clean up the results"""
195 ret = []
196
Mike Frysinger2cd56022017-01-12 20:56:27 -0500197 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400198 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400199 # Gerrit likes to return a stats record too.
200 if not 'project' in cl:
201 continue
202
203 # Strip off common leading names since the result is still
204 # unique over the whole tree.
205 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400206 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
207 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400208 if cl['project'].startswith('%s/' % pfx):
209 cl['project'] = cl['project'][len(pfx) + 1:]
210
Mike Frysinger479f1192017-09-14 22:36:30 -0400211 cl['url'] = uri_lib.ShortenUri(cl['url'])
212
Mike Frysinger13f23a42013-05-13 17:32:01 -0400213 ret.append(cl)
214
Mike Frysingerb62313a2017-06-30 16:38:58 -0400215 if opts.sort == 'unsorted':
216 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700217 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400218 key = lambda x: int(x[opts.sort])
219 else:
220 key = lambda x: x[opts.sort]
221 return sorted(ret, key=key)
222
223
Mike Frysinger13f23a42013-05-13 17:32:01 -0400224def UserActTodo(opts):
225 """List CLs needing your review"""
Mike Frysinger87690552018-12-30 22:56:06 -0500226 cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
Mike Frysingered3d7ea2017-07-10 13:14:02 -0400227 'label:Code-Review=0,user=self '
228 'NOT label:Verified<0'))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400229 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400230
231
Mike Frysingera1db2c42014-06-15 00:42:48 -0700232def UserActSearch(opts, query):
Harry Cutts26076b32019-02-26 15:01:29 -0800233 """List CLs matching the search query"""
Mike Frysingera1db2c42014-06-15 00:42:48 -0700234 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400235 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800236UserActSearch.usage = '<query>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400237
238
Mike Frysingera1db2c42014-06-15 00:42:48 -0700239def UserActMine(opts):
240 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700241 if opts.draft:
242 rule = 'is:draft'
243 else:
244 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500245 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700246
247
Paul Hobbs89765232015-06-24 14:07:49 -0700248def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
249 """Runs breadth first search starting from the nodes in |to_visit|
250
251 Args:
252 to_visit: the starting nodes
253 children: a function which takes a node and returns the nodes adjacent to it
254 visited_key: a function for deduplicating node visits. Defaults to the
255 identity function (lambda x: x)
256
257 Returns:
258 A list of nodes which are reachable from any node in |to_visit| by calling
259 |children| any number of times.
260 """
261 to_visit = list(to_visit)
262 seen = set(map(visited_key, to_visit))
263 for node in to_visit:
264 for child in children(node):
265 key = visited_key(child)
266 if key not in seen:
267 seen.add(key)
268 to_visit.append(child)
269 return to_visit
270
271
272def UserActDeps(opts, query):
273 """List CLs matching a query, and all transitive dependencies of those CLs"""
274 cls = _Query(opts, query, raw=False)
275
Mike Frysinger10666292018-07-12 01:03:38 -0400276 @memoize.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400277 def _QueryChange(cl, helper=None):
278 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700279
Mike Frysinger5726da92017-09-20 22:14:25 -0400280 def _ProcessDeps(cl, deps, required):
281 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700282 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400283 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400284 if not dep.remote in opts.gerrit:
285 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
286 remote=dep.remote, print_cmd=opts.debug)
287 helper = opts.gerrit[dep.remote]
288
Paul Hobbs89765232015-06-24 14:07:49 -0700289 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400290 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
291
292 # Handle empty results. If we found a commit that was pushed directly
293 # (e.g. a bot commit), then gerrit won't know about it.
294 if not changes:
295 if required:
296 logging.error('CL %s depends on %s which cannot be found',
297 cl, dep.ToGerritQueryText())
298 continue
299
300 # Our query might have matched more than one result. This can come up
301 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
302 # across multiple repos/branches. We blindly check all of them in the
303 # hopes that all open ones are what the user wants, but then again the
304 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
305 if len(changes) > 1:
306 logging.warning('CL %s has an ambiguous CQ dependency %s',
307 cl, dep.ToGerritQueryText())
308 for change in changes:
309 if change.status == 'NEW':
310 yield change
311
312 def _Children(cl):
313 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
314 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
315 yield change
316 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
317 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700318
319 transitives = _BreadthFirstSearch(
320 cls, _Children,
321 visited_key=lambda cl: cl.gerrit_number)
322
323 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400324 PrintCls(opts, transitives_raw)
Harry Cutts26076b32019-02-26 15:01:29 -0800325UserActDeps.usage = '<query>'
Paul Hobbs89765232015-06-24 14:07:49 -0700326
327
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700328def UserActInspect(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800329 """Show the details of one or more CLs"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400330 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700331 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400332 helper, cl = GetGerrit(opts, arg)
333 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
334 if change:
335 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700336 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400337 logging.warning('no results found for CL %s', arg)
338 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800339UserActInspect.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400340
341
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700342def UserActReview(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800343 """Mark CLs with a code review status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700344 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700345 for arg in args[:-1]:
346 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800347 helper.SetReview(cl, labels={'Code-Review': num},
348 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700349UserActReview.arg_min = 2
Harry Cutts26076b32019-02-26 15:01:29 -0800350UserActReview.usage = '<CLs...> <-2|-1|0|1|2>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400351
352
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700353def UserActVerify(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800354 """Mark CLs with a verified status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700355 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700356 for arg in args[:-1]:
357 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800358 helper.SetReview(cl, labels={'Verified': num},
359 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700360UserActVerify.arg_min = 2
Harry Cutts26076b32019-02-26 15:01:29 -0800361UserActVerify.usage = '<CLs...> <-1|0|1>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400362
363
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700364def UserActReady(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800365 """Mark CLs with a ready status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700366 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700367 for arg in args[:-1]:
368 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800369 helper.SetReview(cl, labels={'Commit-Queue': num},
370 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700371UserActReady.arg_min = 2
Harry Cutts26076b32019-02-26 15:01:29 -0800372UserActReady.usage = '<CLs...> <0|1>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400373
374
Jason D. Clinton8a84f7c2019-03-27 17:03:57 -0600375def UserActTryready(opts, *args):
376 """Mark CLs with a CQ+1 try status"""
Mike Frysinger15b23e42014-12-05 17:00:05 -0500377 num = args[-1]
378 for arg in args[:-1]:
379 helper, cl = GetGerrit(opts, arg)
Jason D. Clinton8a84f7c2019-03-27 17:03:57 -0600380 helper.SetReview(cl, labels={'Commit-Queue': num},
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800381 dryrun=opts.dryrun, notify=opts.notify)
Jason D. Clinton8a84f7c2019-03-27 17:03:57 -0600382UserActTryready.arg_min = 2
383UserActTryready.usage = '<CLs...> <0|1>'
Mike Frysinger15b23e42014-12-05 17:00:05 -0500384
385
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700386def UserActSubmit(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800387 """Submit CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700388 for arg in args:
389 helper, cl = GetGerrit(opts, arg)
390 helper.SubmitChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800391UserActSubmit.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400392
393
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700394def UserActAbandon(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800395 """Abandon CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700396 for arg in args:
397 helper, cl = GetGerrit(opts, arg)
398 helper.AbandonChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800399UserActAbandon.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400400
401
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700402def UserActRestore(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800403 """Restore CLs that were abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700404 for arg in args:
405 helper, cl = GetGerrit(opts, arg)
406 helper.RestoreChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800407UserActRestore.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400408
409
Mike Frysinger88f27292014-06-17 09:40:45 -0700410def UserActReviewers(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800411 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500412 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700413 # Allow for optional leading '~'.
414 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
415 add_list, remove_list, invalid_list = [], [], []
416
417 for x in emails:
418 if not email_validator.match(x):
419 invalid_list.append(x)
420 elif x[0] == '~':
421 remove_list.append(x[1:])
422 else:
423 add_list.append(x)
424
425 if invalid_list:
426 cros_build_lib.Die(
427 'Invalid email address(es): %s' % ', '.join(invalid_list))
428
429 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700430 helper, cl = GetGerrit(opts, cl)
431 helper.SetReviewers(cl, add=add_list, remove=remove_list,
432 dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800433UserActReviewers.usage = '<CL> <emails...>'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700434
435
Allen Li38abdaa2017-03-16 13:25:02 -0700436def UserActAssign(opts, cl, assignee):
Harry Cutts26076b32019-02-26 15:01:29 -0800437 """Set the assignee for a CL"""
Allen Li38abdaa2017-03-16 13:25:02 -0700438 helper, cl = GetGerrit(opts, cl)
439 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800440UserActAssign.usage = '<CL> <assignee>'
Allen Li38abdaa2017-03-16 13:25:02 -0700441
442
Mike Frysinger88f27292014-06-17 09:40:45 -0700443def UserActMessage(opts, cl, message):
Harry Cutts26076b32019-02-26 15:01:29 -0800444 """Add a message to a CL"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700445 helper, cl = GetGerrit(opts, cl)
446 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800447UserActMessage.usage = '<CL> <message>'
Doug Anderson8119df02013-07-20 21:00:24 +0530448
449
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800450def UserActTopic(opts, topic, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800451 """Set a topic for one or more CLs"""
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800452 for arg in args:
453 helper, arg = GetGerrit(opts, arg)
454 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800455UserActTopic.usage = '<topic> <CLs...>'
456
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800457
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700458def UserActPrivate(opts, cl, private_str):
Harry Cutts26076b32019-02-26 15:01:29 -0800459 """Set the private bit on a CL to private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700460 try:
461 private = cros_build_lib.BooleanShellValue(private_str, False)
462 except ValueError:
463 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
464
465 helper, cl = GetGerrit(opts, cl)
466 helper.SetPrivate(cl, private)
Harry Cutts26076b32019-02-26 15:01:29 -0800467UserActPrivate.usage = '<CL> <private str>'
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700468
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800469
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800470def UserActSethashtags(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800471 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800472 hashtags = args
473 add = []
474 remove = []
475 for hashtag in hashtags:
476 if hashtag.startswith('~'):
477 remove.append(hashtag[1:])
478 else:
479 add.append(hashtag)
480 helper, cl = GetGerrit(opts, cl)
481 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800482UserActSethashtags.usage = '<CL> <hashtags...>'
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800483
484
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700485def UserActDeletedraft(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800486 """Delete draft CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700487 for arg in args:
488 helper, cl = GetGerrit(opts, arg)
489 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800490UserActDeletedraft.usage = '<CLs...>'
Jon Salza427fb02014-03-07 18:13:17 +0800491
492
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800493def UserActAccount(opts):
Harry Cutts26076b32019-02-26 15:01:29 -0800494 """Get the current user account information"""
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800495 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400496 acct = helper.GetAccount()
497 if opts.json:
498 json.dump(acct, sys.stdout)
499 else:
500 print('account_id:%i %s <%s>' %
501 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800502
503
Harry Cutts26076b32019-02-26 15:01:29 -0800504def _GetActionUsages():
505 """Formats a one-line usage and doc message for each action."""
506 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
507 actions.sort()
508
509 cmds = [x[len(ACTION_PREFIX):] for x in actions]
510
511 # Sanity check names for devs adding new commands. Should be quick.
512 for cmd in cmds:
513 expected_name = cmd.lower().capitalize()
514 if cmd != expected_name:
515 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
516 (cmd, expected_name))
517
518 functions = [globals()[x] for x in actions]
519 usages = [getattr(x, 'usage', '') for x in functions]
520 docs = [x.__doc__ for x in functions]
521
522 action_usages = []
523 cmd_indent = len(max(cmds, key=len))
524 usage_indent = len(max(usages, key=len))
525 for cmd, usage, doc in zip(cmds, usages, docs):
526 action_usages.append(' %-*s %-*s : %s' %
527 (cmd_indent, cmd.lower(), usage_indent, usage, doc))
528
529 return '\n'.join(action_usages)
530
531
Mike Frysinger108eda22018-06-06 18:45:12 -0400532def GetParser():
533 """Returns the parser to use for this module."""
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500534 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400535
536There is no support for doing line-by-line code review via the command line.
537This helps you manage various bits and CL status.
538
Mike Frysingera1db2c42014-06-15 00:42:48 -0700539For general Gerrit documentation, see:
540 https://gerrit-review.googlesource.com/Documentation/
541The Searching Changes page covers the search query syntax:
542 https://gerrit-review.googlesource.com/Documentation/user-search.html
543
Mike Frysinger13f23a42013-05-13 17:32:01 -0400544Example:
545 $ gerrit todo # List all the CLs that await your review.
546 $ gerrit mine # List all of your open CLs.
547 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
548 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
549 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700550Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700551 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
552ready.
553 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
554ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400555 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400556
Harry Cutts26076b32019-02-26 15:01:29 -0800557Actions:
558"""
559 usage += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400560
Alex Klein2ab29cc2018-07-19 12:01:00 -0600561 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500562 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500563 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600564 default=site_params.EXTERNAL_GOB_INSTANCE,
565 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500566 help='Query internal Chromium Gerrit instance')
567 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600568 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500569 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600570 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500571 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400572 help='Key to sort on (number, project); use "unsorted" '
573 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700574 parser.add_argument('--raw', default=False, action='store_true',
575 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400576 parser.add_argument('--json', default=False, action='store_true',
577 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700578 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
579 dest='dryrun',
580 help='Show what would be done, but do not make changes')
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800581 parser.add_argument('--ne', '--no-emails', default=True, action='store_false',
582 dest='send_email',
583 help='Do not send email for some operations '
584 '(e.g. ready/review/trybotready/verify)')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500585 parser.add_argument('-v', '--verbose', default=False, action='store_true',
586 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800587 parser.add_argument('-b', '--branch',
588 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700589 parser.add_argument('--draft', default=False, action='store_true',
590 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800591 parser.add_argument('-p', '--project',
592 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800593 parser.add_argument('-t', '--topic',
594 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500595 parser.add_argument('action', help='The gerrit action to perform')
596 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400597
598 return parser
599
600
601def main(argv):
602 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500603 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400604
Mike Frysinger88f27292014-06-17 09:40:45 -0700605 # A cache of gerrit helpers we'll load on demand.
606 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800607
608 # Convert user friendly command line option into a gerrit parameter.
609 opts.notify = 'ALL' if opts.send_email else 'NONE'
Mike Frysinger88f27292014-06-17 09:40:45 -0700610 opts.Freeze()
611
Mike Frysinger27e21b72018-07-12 14:20:21 -0400612 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400613 global COLOR
614 COLOR = terminal.Color(enabled=opts.color)
615
Mike Frysinger13f23a42013-05-13 17:32:01 -0400616 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400617 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400618 if functor:
619 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700620 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700621 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500622 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700623 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500624 (opts.action, arg_min))
625 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400626 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500627 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700628 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500629 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500630 except (cros_build_lib.RunCommandError, gerrit.GerritException,
631 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700632 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400633 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500634 parser.error('unknown action: %s' % (opts.action,))