blob: 1d0565d3a9a5b7bb8d115ff408580e2efb755068 [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 ',],
Jason D. Clinton729f81f2019-05-02 20:24:33 -060045 'LCQ': ['L ', 'Legacy ',],
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
Mike Frysinger4aea5dc2019-07-17 13:39:56 -040052# Shorter strings for CL status messages.
53GERRIT_SUMMARY_MAP = {
54 'ABANDONED': 'ABD',
55 'MERGED': 'MRG',
56 'NEW': 'NEW',
57 'WIP': 'WIP',
58}
59
Mike Frysinger13f23a42013-05-13 17:32:01 -040060
61def red(s):
62 return COLOR.Color(terminal.Color.RED, s)
63
64
65def green(s):
66 return COLOR.Color(terminal.Color.GREEN, s)
67
68
69def blue(s):
70 return COLOR.Color(terminal.Color.BLUE, s)
71
72
73def limits(cls):
74 """Given a dict of fields, calculate the longest string lengths
75
76 This allows you to easily format the output of many results so that the
77 various cols all line up correctly.
78 """
79 lims = {}
80 for cl in cls:
81 for k in cl.keys():
Mike Frysingerf16b8f02013-10-21 22:24:46 -040082 # Use %s rather than str() to avoid codec issues.
83 # We also do this so we can format integers.
84 lims[k] = max(lims.get(k, 0), len('%s' % cl[k]))
Mike Frysinger13f23a42013-05-13 17:32:01 -040085 return lims
86
87
Mike Frysinger88f27292014-06-17 09:40:45 -070088# TODO: This func really needs to be merged into the core gerrit logic.
89def GetGerrit(opts, cl=None):
90 """Auto pick the right gerrit instance based on the |cl|
91
92 Args:
93 opts: The general options object.
94 cl: A CL taking one of the forms: 1234 *1234 chromium:1234
95
96 Returns:
97 A tuple of a gerrit object and a sanitized CL #.
98 """
99 gob = opts.gob
Paul Hobbs89765232015-06-24 14:07:49 -0700100 if cl is not None:
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600101 if cl.startswith('*') or cl.startswith('chrome-internal:'):
Alex Klein2ab29cc2018-07-19 12:01:00 -0600102 gob = config_lib.GetSiteParams().INTERNAL_GOB_INSTANCE
Jason D. Clintoneb1073d2019-04-13 02:33:20 -0600103 if cl.startswith('*'):
104 cl = cl[1:]
105 else:
106 cl = cl[16:]
Mike Frysinger88f27292014-06-17 09:40:45 -0700107 elif ':' in cl:
108 gob, cl = cl.split(':', 1)
109
110 if not gob in opts.gerrit:
111 opts.gerrit[gob] = gerrit.GetGerritHelper(gob=gob, print_cmd=opts.debug)
112
113 return (opts.gerrit[gob], cl)
114
115
Mike Frysinger13f23a42013-05-13 17:32:01 -0400116def GetApprovalSummary(_opts, cls):
117 """Return a dict of the most important approvals"""
118 approvs = dict([(x, '') for x in GERRIT_SUMMARY_CATS])
Aviv Keshetad30cec2018-09-27 18:12:15 -0700119 for approver in cls.get('currentPatchSet', {}).get('approvals', []):
120 cats = GERRIT_APPROVAL_MAP.get(approver['type'])
121 if not cats:
122 logging.warning('unknown gerrit approval type: %s', approver['type'])
123 continue
124 cat = cats[0].strip()
125 val = int(approver['value'])
126 if not cat in approvs:
127 # Ignore the extended categories in the summary view.
128 continue
129 elif approvs[cat] == '':
130 approvs[cat] = val
131 elif val < 0:
132 approvs[cat] = min(approvs[cat], val)
133 else:
134 approvs[cat] = max(approvs[cat], val)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400135 return approvs
136
137
Mike Frysingera1b4b272017-04-05 16:11:00 -0400138def PrettyPrintCl(opts, cl, lims=None, show_approvals=True):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400139 """Pretty print a single result"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400140 if lims is None:
Mike Frysinger13f23a42013-05-13 17:32:01 -0400141 lims = {'url': 0, 'project': 0}
142
143 status = ''
Mike Frysinger4aea5dc2019-07-17 13:39:56 -0400144
145 if opts.verbose:
146 status += '%s ' % (cl['status'],)
147 else:
148 status += '%s ' % (GERRIT_SUMMARY_MAP.get(cl['status'], cl['status']),)
149
Mike Frysinger13f23a42013-05-13 17:32:01 -0400150 if show_approvals and not opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400151 approvs = GetApprovalSummary(opts, cl)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400152 for cat in GERRIT_SUMMARY_CATS:
Mike Frysingera0313d02017-07-10 16:44:43 -0400153 if approvs[cat] in ('', 0):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400154 functor = lambda x: x
155 elif approvs[cat] < 0:
156 functor = red
157 else:
158 functor = green
159 status += functor('%s:%2s ' % (cat, approvs[cat]))
160
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400161 print('%s %s%-*s %s' % (blue('%-*s' % (lims['url'], cl['url'])), status,
162 lims['project'], cl['project'], cl['subject']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400163
164 if show_approvals and opts.verbose:
Mike Frysingerb4a3e3c2017-04-05 16:06:53 -0400165 for approver in cl['currentPatchSet'].get('approvals', []):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400166 functor = red if int(approver['value']) < 0 else green
167 n = functor('%2s' % approver['value'])
168 t = GERRIT_APPROVAL_MAP.get(approver['type'], [approver['type'],
169 approver['type']])[1]
Mike Frysinger31ff6f92014-02-08 04:33:03 -0500170 print(' %s %s %s' % (n, t, approver['by']['email']))
Mike Frysinger13f23a42013-05-13 17:32:01 -0400171
172
Mike Frysingera1b4b272017-04-05 16:11:00 -0400173def PrintCls(opts, cls, lims=None, show_approvals=True):
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400174 """Print all results based on the requested format."""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400175 if opts.raw:
Alex Klein2ab29cc2018-07-19 12:01:00 -0600176 site_params = config_lib.GetSiteParams()
Mike Frysingera1b4b272017-04-05 16:11:00 -0400177 pfx = ''
178 # Special case internal Chrome GoB as that is what most devs use.
179 # They can always redirect the list elsewhere via the -g option.
Alex Klein2ab29cc2018-07-19 12:01:00 -0600180 if opts.gob == site_params.INTERNAL_GOB_INSTANCE:
181 pfx = site_params.INTERNAL_CHANGE_PREFIX
Mike Frysingera1b4b272017-04-05 16:11:00 -0400182 for cl in cls:
183 print('%s%s' % (pfx, cl['number']))
184
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400185 elif opts.json:
186 json.dump(cls, sys.stdout)
187
Mike Frysingera1b4b272017-04-05 16:11:00 -0400188 else:
189 if lims is None:
190 lims = limits(cls)
191
192 for cl in cls:
193 PrettyPrintCl(opts, cl, lims=lims, show_approvals=show_approvals)
194
195
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400196def _Query(opts, query, raw=True, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700197 """Queries Gerrit with a query string built from the commandline options"""
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800198 if opts.branch is not None:
199 query += ' branch:%s' % opts.branch
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800200 if opts.project is not None:
201 query += ' project: %s' % opts.project
Mathieu Olivari14645a12015-01-16 15:41:32 -0800202 if opts.topic is not None:
203 query += ' topic: %s' % opts.topic
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800204
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400205 if helper is None:
206 helper, _ = GetGerrit(opts)
Paul Hobbs89765232015-06-24 14:07:49 -0700207 return helper.Query(query, raw=raw, bypass_cache=False)
208
209
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400210def FilteredQuery(opts, query, helper=None):
Paul Hobbs89765232015-06-24 14:07:49 -0700211 """Query gerrit and filter/clean up the results"""
212 ret = []
213
Mike Frysinger2cd56022017-01-12 20:56:27 -0500214 logging.debug('Running query: %s', query)
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400215 for cl in _Query(opts, query, raw=True, helper=helper):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400216 # Gerrit likes to return a stats record too.
217 if not 'project' in cl:
218 continue
219
220 # Strip off common leading names since the result is still
221 # unique over the whole tree.
222 if not opts.verbose:
Mike Frysinger1d508282018-06-07 16:59:44 -0400223 for pfx in ('aosp', 'chromeos', 'chromiumos', 'external', 'overlays',
224 'platform', 'third_party'):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400225 if cl['project'].startswith('%s/' % pfx):
226 cl['project'] = cl['project'][len(pfx) + 1:]
227
Mike Frysinger479f1192017-09-14 22:36:30 -0400228 cl['url'] = uri_lib.ShortenUri(cl['url'])
229
Mike Frysinger13f23a42013-05-13 17:32:01 -0400230 ret.append(cl)
231
Mike Frysingerb62313a2017-06-30 16:38:58 -0400232 if opts.sort == 'unsorted':
233 return ret
Paul Hobbs89765232015-06-24 14:07:49 -0700234 if opts.sort == 'number':
Mike Frysinger13f23a42013-05-13 17:32:01 -0400235 key = lambda x: int(x[opts.sort])
236 else:
237 key = lambda x: x[opts.sort]
238 return sorted(ret, key=key)
239
240
Mike Frysinger13f23a42013-05-13 17:32:01 -0400241def UserActTodo(opts):
242 """List CLs needing your review"""
Mike Frysinger87690552018-12-30 22:56:06 -0500243 cls = FilteredQuery(opts, ('reviewer:self status:open NOT owner:self '
Mike Frysingered3d7ea2017-07-10 13:14:02 -0400244 'label:Code-Review=0,user=self '
245 'NOT label:Verified<0'))
Mike Frysingera1b4b272017-04-05 16:11:00 -0400246 PrintCls(opts, cls)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400247
248
Mike Frysingera1db2c42014-06-15 00:42:48 -0700249def UserActSearch(opts, query):
Harry Cutts26076b32019-02-26 15:01:29 -0800250 """List CLs matching the search query"""
Mike Frysingera1db2c42014-06-15 00:42:48 -0700251 cls = FilteredQuery(opts, query)
Mike Frysingera1b4b272017-04-05 16:11:00 -0400252 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800253UserActSearch.usage = '<query>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400254
255
Mike Frysingera1db2c42014-06-15 00:42:48 -0700256def UserActMine(opts):
257 """List your CLs with review statuses"""
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700258 if opts.draft:
259 rule = 'is:draft'
260 else:
261 rule = 'status:new'
Mike Frysinger2cd56022017-01-12 20:56:27 -0500262 UserActSearch(opts, 'owner:self %s' % (rule,))
Mike Frysingera1db2c42014-06-15 00:42:48 -0700263
264
Paul Hobbs89765232015-06-24 14:07:49 -0700265def _BreadthFirstSearch(to_visit, children, visited_key=lambda x: x):
266 """Runs breadth first search starting from the nodes in |to_visit|
267
268 Args:
269 to_visit: the starting nodes
270 children: a function which takes a node and returns the nodes adjacent to it
271 visited_key: a function for deduplicating node visits. Defaults to the
272 identity function (lambda x: x)
273
274 Returns:
275 A list of nodes which are reachable from any node in |to_visit| by calling
276 |children| any number of times.
277 """
278 to_visit = list(to_visit)
279 seen = set(map(visited_key, to_visit))
280 for node in to_visit:
281 for child in children(node):
282 key = visited_key(child)
283 if key not in seen:
284 seen.add(key)
285 to_visit.append(child)
286 return to_visit
287
288
289def UserActDeps(opts, query):
290 """List CLs matching a query, and all transitive dependencies of those CLs"""
291 cls = _Query(opts, query, raw=False)
292
Mike Frysinger10666292018-07-12 01:03:38 -0400293 @memoize.Memoize
Mike Frysingerb3300c42017-07-20 01:41:17 -0400294 def _QueryChange(cl, helper=None):
295 return _Query(opts, cl, raw=False, helper=helper)
Paul Hobbs89765232015-06-24 14:07:49 -0700296
Mike Frysinger5726da92017-09-20 22:14:25 -0400297 def _ProcessDeps(cl, deps, required):
298 """Yields matching dependencies for a patch"""
Paul Hobbs89765232015-06-24 14:07:49 -0700299 # We need to query the change to guarantee that we have a .gerrit_number
Mike Frysinger5726da92017-09-20 22:14:25 -0400300 for dep in deps:
Mike Frysingerb3300c42017-07-20 01:41:17 -0400301 if not dep.remote in opts.gerrit:
302 opts.gerrit[dep.remote] = gerrit.GetGerritHelper(
303 remote=dep.remote, print_cmd=opts.debug)
304 helper = opts.gerrit[dep.remote]
305
Paul Hobbs89765232015-06-24 14:07:49 -0700306 # TODO(phobbs) this should maybe catch network errors.
Mike Frysinger5726da92017-09-20 22:14:25 -0400307 changes = _QueryChange(dep.ToGerritQueryText(), helper=helper)
308
309 # Handle empty results. If we found a commit that was pushed directly
310 # (e.g. a bot commit), then gerrit won't know about it.
311 if not changes:
312 if required:
313 logging.error('CL %s depends on %s which cannot be found',
314 cl, dep.ToGerritQueryText())
315 continue
316
317 # Our query might have matched more than one result. This can come up
318 # when CQ-DEPEND uses a Gerrit Change-Id, but that Change-Id shows up
319 # across multiple repos/branches. We blindly check all of them in the
320 # hopes that all open ones are what the user wants, but then again the
321 # CQ-DEPEND syntax itself is unable to differeniate. *shrug*
322 if len(changes) > 1:
323 logging.warning('CL %s has an ambiguous CQ dependency %s',
324 cl, dep.ToGerritQueryText())
325 for change in changes:
326 if change.status == 'NEW':
327 yield change
328
329 def _Children(cl):
330 """Yields the Gerrit and CQ-Depends dependencies of a patch"""
331 for change in _ProcessDeps(cl, cl.PaladinDependencies(None), True):
332 yield change
333 for change in _ProcessDeps(cl, cl.GerritDependencies(), False):
334 yield change
Paul Hobbs89765232015-06-24 14:07:49 -0700335
336 transitives = _BreadthFirstSearch(
337 cls, _Children,
338 visited_key=lambda cl: cl.gerrit_number)
339
340 transitives_raw = [cl.patch_dict for cl in transitives]
Mike Frysingera1b4b272017-04-05 16:11:00 -0400341 PrintCls(opts, transitives_raw)
Harry Cutts26076b32019-02-26 15:01:29 -0800342UserActDeps.usage = '<query>'
Paul Hobbs89765232015-06-24 14:07:49 -0700343
344
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700345def UserActInspect(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800346 """Show the details of one or more CLs"""
Mike Frysingera1b4b272017-04-05 16:11:00 -0400347 cls = []
Mike Frysinger88f27292014-06-17 09:40:45 -0700348 for arg in args:
Mike Frysinger5f938ca2017-07-19 18:29:02 -0400349 helper, cl = GetGerrit(opts, arg)
350 change = FilteredQuery(opts, 'change:%s' % cl, helper=helper)
351 if change:
352 cls.extend(change)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700353 else:
Mike Frysingera1b4b272017-04-05 16:11:00 -0400354 logging.warning('no results found for CL %s', arg)
355 PrintCls(opts, cls)
Harry Cutts26076b32019-02-26 15:01:29 -0800356UserActInspect.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400357
358
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700359def UserActReview(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800360 """Mark CLs with a code review status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700361 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700362 for arg in args[:-1]:
363 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800364 helper.SetReview(cl, labels={'Code-Review': num},
365 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700366UserActReview.arg_min = 2
Harry Cutts26076b32019-02-26 15:01:29 -0800367UserActReview.usage = '<CLs...> <-2|-1|0|1|2>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400368
369
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700370def UserActVerify(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800371 """Mark CLs with a verified status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700372 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700373 for arg in args[:-1]:
374 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800375 helper.SetReview(cl, labels={'Verified': num},
376 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700377UserActVerify.arg_min = 2
Harry Cutts26076b32019-02-26 15:01:29 -0800378UserActVerify.usage = '<CLs...> <-1|0|1>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400379
380
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700381def UserActReady(opts, *args):
Jason D. Clinton88aed612019-04-07 20:24:05 -0600382 """Mark CLs with CQ dryrun (1) or ready (2) status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700383 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700384 for arg in args[:-1]:
385 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800386 helper.SetReview(cl, labels={'Commit-Queue': num},
387 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700388UserActReady.arg_min = 2
Jason D. Clinton88aed612019-04-07 20:24:05 -0600389UserActReady.usage = '<CLs...> <0|1|2>'
Mike Frysinger15b23e42014-12-05 17:00:05 -0500390
391
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700392def UserActSubmit(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800393 """Submit CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700394 for arg in args:
395 helper, cl = GetGerrit(opts, arg)
396 helper.SubmitChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800397UserActSubmit.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400398
399
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700400def UserActAbandon(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800401 """Abandon CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700402 for arg in args:
403 helper, cl = GetGerrit(opts, arg)
404 helper.AbandonChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800405UserActAbandon.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400406
407
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700408def UserActRestore(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800409 """Restore CLs that were abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700410 for arg in args:
411 helper, cl = GetGerrit(opts, arg)
412 helper.RestoreChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800413UserActRestore.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400414
415
Mike Frysinger88f27292014-06-17 09:40:45 -0700416def UserActReviewers(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800417 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500418 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700419 # Allow for optional leading '~'.
420 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
421 add_list, remove_list, invalid_list = [], [], []
422
423 for x in emails:
424 if not email_validator.match(x):
425 invalid_list.append(x)
426 elif x[0] == '~':
427 remove_list.append(x[1:])
428 else:
429 add_list.append(x)
430
431 if invalid_list:
432 cros_build_lib.Die(
433 'Invalid email address(es): %s' % ', '.join(invalid_list))
434
435 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700436 helper, cl = GetGerrit(opts, cl)
437 helper.SetReviewers(cl, add=add_list, remove=remove_list,
438 dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800439UserActReviewers.usage = '<CL> <emails...>'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700440
441
Allen Li38abdaa2017-03-16 13:25:02 -0700442def UserActAssign(opts, cl, assignee):
Harry Cutts26076b32019-02-26 15:01:29 -0800443 """Set the assignee for a CL"""
Allen Li38abdaa2017-03-16 13:25:02 -0700444 helper, cl = GetGerrit(opts, cl)
445 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800446UserActAssign.usage = '<CL> <assignee>'
Allen Li38abdaa2017-03-16 13:25:02 -0700447
448
Mike Frysinger88f27292014-06-17 09:40:45 -0700449def UserActMessage(opts, cl, message):
Harry Cutts26076b32019-02-26 15:01:29 -0800450 """Add a message to a CL"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700451 helper, cl = GetGerrit(opts, cl)
452 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800453UserActMessage.usage = '<CL> <message>'
Doug Anderson8119df02013-07-20 21:00:24 +0530454
455
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800456def UserActTopic(opts, topic, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800457 """Set a topic for one or more CLs"""
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800458 for arg in args:
459 helper, arg = GetGerrit(opts, arg)
460 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800461UserActTopic.usage = '<topic> <CLs...>'
462
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800463
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700464def UserActPrivate(opts, cl, private_str):
Harry Cutts26076b32019-02-26 15:01:29 -0800465 """Set the private bit on a CL to private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700466 try:
467 private = cros_build_lib.BooleanShellValue(private_str, False)
468 except ValueError:
469 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
470
471 helper, cl = GetGerrit(opts, cl)
472 helper.SetPrivate(cl, private)
Harry Cutts26076b32019-02-26 15:01:29 -0800473UserActPrivate.usage = '<CL> <private str>'
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700474
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800475
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800476def UserActSethashtags(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800477 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800478 hashtags = args
479 add = []
480 remove = []
481 for hashtag in hashtags:
482 if hashtag.startswith('~'):
483 remove.append(hashtag[1:])
484 else:
485 add.append(hashtag)
486 helper, cl = GetGerrit(opts, cl)
487 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800488UserActSethashtags.usage = '<CL> <hashtags...>'
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800489
490
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700491def UserActDeletedraft(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800492 """Delete draft CLs"""
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)
Harry Cutts26076b32019-02-26 15:01:29 -0800496UserActDeletedraft.usage = '<CLs...>'
Jon Salza427fb02014-03-07 18:13:17 +0800497
498
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800499def UserActAccount(opts):
Harry Cutts26076b32019-02-26 15:01:29 -0800500 """Get the current user account information"""
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800501 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400502 acct = helper.GetAccount()
503 if opts.json:
504 json.dump(acct, sys.stdout)
505 else:
506 print('account_id:%i %s <%s>' %
507 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800508
509
Harry Cutts26076b32019-02-26 15:01:29 -0800510def _GetActionUsages():
511 """Formats a one-line usage and doc message for each action."""
512 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
513 actions.sort()
514
515 cmds = [x[len(ACTION_PREFIX):] for x in actions]
516
517 # Sanity check names for devs adding new commands. Should be quick.
518 for cmd in cmds:
519 expected_name = cmd.lower().capitalize()
520 if cmd != expected_name:
521 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
522 (cmd, expected_name))
523
524 functions = [globals()[x] for x in actions]
525 usages = [getattr(x, 'usage', '') for x in functions]
526 docs = [x.__doc__ for x in functions]
527
528 action_usages = []
529 cmd_indent = len(max(cmds, key=len))
530 usage_indent = len(max(usages, key=len))
531 for cmd, usage, doc in zip(cmds, usages, docs):
532 action_usages.append(' %-*s %-*s : %s' %
533 (cmd_indent, cmd.lower(), usage_indent, usage, doc))
534
535 return '\n'.join(action_usages)
536
537
Mike Frysinger108eda22018-06-06 18:45:12 -0400538def GetParser():
539 """Returns the parser to use for this module."""
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500540 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400541
542There is no support for doing line-by-line code review via the command line.
543This helps you manage various bits and CL status.
544
Mike Frysingera1db2c42014-06-15 00:42:48 -0700545For general Gerrit documentation, see:
546 https://gerrit-review.googlesource.com/Documentation/
547The Searching Changes page covers the search query syntax:
548 https://gerrit-review.googlesource.com/Documentation/user-search.html
549
Mike Frysinger13f23a42013-05-13 17:32:01 -0400550Example:
551 $ gerrit todo # List all the CLs that await your review.
552 $ gerrit mine # List all of your open CLs.
553 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
554 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
555 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800556 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
55728123.
558 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
559CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700560Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700561 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
562ready.
563 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
564ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400565 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400566
Harry Cutts26076b32019-02-26 15:01:29 -0800567Actions:
568"""
569 usage += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400570
Alex Klein2ab29cc2018-07-19 12:01:00 -0600571 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500572 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500573 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600574 default=site_params.EXTERNAL_GOB_INSTANCE,
575 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500576 help='Query internal Chromium Gerrit instance')
577 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600578 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500579 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600580 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500581 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400582 help='Key to sort on (number, project); use "unsorted" '
583 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700584 parser.add_argument('--raw', default=False, action='store_true',
585 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400586 parser.add_argument('--json', default=False, action='store_true',
587 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700588 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
589 dest='dryrun',
590 help='Show what would be done, but do not make changes')
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800591 parser.add_argument('--ne', '--no-emails', default=True, action='store_false',
592 dest='send_email',
593 help='Do not send email for some operations '
594 '(e.g. ready/review/trybotready/verify)')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500595 parser.add_argument('-v', '--verbose', default=False, action='store_true',
596 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800597 parser.add_argument('-b', '--branch',
598 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700599 parser.add_argument('--draft', default=False, action='store_true',
600 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800601 parser.add_argument('-p', '--project',
602 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800603 parser.add_argument('-t', '--topic',
604 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500605 parser.add_argument('action', help='The gerrit action to perform')
606 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400607
608 return parser
609
610
611def main(argv):
612 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500613 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400614
Mike Frysinger88f27292014-06-17 09:40:45 -0700615 # A cache of gerrit helpers we'll load on demand.
616 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800617
618 # Convert user friendly command line option into a gerrit parameter.
619 opts.notify = 'ALL' if opts.send_email else 'NONE'
Mike Frysinger88f27292014-06-17 09:40:45 -0700620 opts.Freeze()
621
Mike Frysinger27e21b72018-07-12 14:20:21 -0400622 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400623 global COLOR
624 COLOR = terminal.Color(enabled=opts.color)
625
Mike Frysinger13f23a42013-05-13 17:32:01 -0400626 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400627 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400628 if functor:
629 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700630 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700631 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500632 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700633 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500634 (opts.action, arg_min))
635 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400636 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500637 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700638 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500639 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500640 except (cros_build_lib.RunCommandError, gerrit.GerritException,
641 gob_util.GOBError) as e:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700642 cros_build_lib.Die(e.message)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400643 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500644 parser.error('unknown action: %s' % (opts.action,))