blob: 179e647d7105c489643e41e9e2ed853f4de02cf2 [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 Frysinger13f23a42013-05-13 17:32:01 -040027from chromite.lib import terminal
Mike Frysinger479f1192017-09-14 22:36:30 -040028from chromite.lib import uri_lib
Alex Klein337fee42019-07-08 11:38:26 -060029from chromite.utils import memoize
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)
Mike Frysinger66ce4132019-07-17 22:52:52 -0400279 seen = set(visited_key(x) for x in to_visit)
Paul Hobbs89765232015-06-24 14:07:49 -0700280 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
Jack Rosenthal8a1fb542019-08-07 10:23:56 -0600359def UserActAutosubmit(opts, *args):
360 """Mark CLs with the Auto-Submit label"""
361 num = args[-1]
362 for arg in args[:-1]:
363 helper, cl = GetGerrit(opts, arg)
364 helper.SetReview(cl, labels={'Auto-Submit': num},
365 dryrun=opts.dryrun, notify=opts.notify)
366UserActAutosubmit.arg_min = 2
367UserActAutosubmit.usage = '<CLs...> <0|1>'
368
369
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700370def UserActReview(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800371 """Mark CLs with a code review 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={'Code-Review': num},
376 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700377UserActReview.arg_min = 2
Harry Cutts26076b32019-02-26 15:01:29 -0800378UserActReview.usage = '<CLs...> <-2|-1|0|1|2>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400379
380
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700381def UserActVerify(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800382 """Mark CLs with a verified 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={'Verified': num},
387 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700388UserActVerify.arg_min = 2
Harry Cutts26076b32019-02-26 15:01:29 -0800389UserActVerify.usage = '<CLs...> <-1|0|1>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400390
391
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700392def UserActReady(opts, *args):
Jason D. Clinton88aed612019-04-07 20:24:05 -0600393 """Mark CLs with CQ dryrun (1) or ready (2) status"""
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700394 num = args[-1]
Mike Frysinger88f27292014-06-17 09:40:45 -0700395 for arg in args[:-1]:
396 helper, cl = GetGerrit(opts, arg)
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800397 helper.SetReview(cl, labels={'Commit-Queue': num},
398 dryrun=opts.dryrun, notify=opts.notify)
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700399UserActReady.arg_min = 2
Jason D. Clinton88aed612019-04-07 20:24:05 -0600400UserActReady.usage = '<CLs...> <0|1|2>'
Mike Frysinger15b23e42014-12-05 17:00:05 -0500401
402
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700403def UserActSubmit(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800404 """Submit CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700405 for arg in args:
406 helper, cl = GetGerrit(opts, arg)
407 helper.SubmitChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800408UserActSubmit.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400409
410
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700411def UserActAbandon(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800412 """Abandon CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700413 for arg in args:
414 helper, cl = GetGerrit(opts, arg)
415 helper.AbandonChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800416UserActAbandon.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400417
418
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700419def UserActRestore(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800420 """Restore CLs that were abandoned"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700421 for arg in args:
422 helper, cl = GetGerrit(opts, arg)
423 helper.RestoreChange(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800424UserActRestore.usage = '<CLs...>'
Mike Frysinger13f23a42013-05-13 17:32:01 -0400425
426
Mike Frysinger88f27292014-06-17 09:40:45 -0700427def UserActReviewers(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800428 """Add/remove reviewers' emails for a CL (prepend with '~' to remove)"""
Mike Frysingerc15efa52013-12-12 01:13:56 -0500429 emails = args
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700430 # Allow for optional leading '~'.
431 email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
432 add_list, remove_list, invalid_list = [], [], []
433
434 for x in emails:
435 if not email_validator.match(x):
436 invalid_list.append(x)
437 elif x[0] == '~':
438 remove_list.append(x[1:])
439 else:
440 add_list.append(x)
441
442 if invalid_list:
443 cros_build_lib.Die(
444 'Invalid email address(es): %s' % ', '.join(invalid_list))
445
446 if add_list or remove_list:
Mike Frysinger88f27292014-06-17 09:40:45 -0700447 helper, cl = GetGerrit(opts, cl)
448 helper.SetReviewers(cl, add=add_list, remove=remove_list,
449 dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800450UserActReviewers.usage = '<CL> <emails...>'
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700451
452
Allen Li38abdaa2017-03-16 13:25:02 -0700453def UserActAssign(opts, cl, assignee):
Harry Cutts26076b32019-02-26 15:01:29 -0800454 """Set the assignee for a CL"""
Allen Li38abdaa2017-03-16 13:25:02 -0700455 helper, cl = GetGerrit(opts, cl)
456 helper.SetAssignee(cl, assignee, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800457UserActAssign.usage = '<CL> <assignee>'
Allen Li38abdaa2017-03-16 13:25:02 -0700458
459
Mike Frysinger88f27292014-06-17 09:40:45 -0700460def UserActMessage(opts, cl, message):
Harry Cutts26076b32019-02-26 15:01:29 -0800461 """Add a message to a CL"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700462 helper, cl = GetGerrit(opts, cl)
463 helper.SetReview(cl, msg=message, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800464UserActMessage.usage = '<CL> <message>'
Doug Anderson8119df02013-07-20 21:00:24 +0530465
466
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800467def UserActTopic(opts, topic, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800468 """Set a topic for one or more CLs"""
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800469 for arg in args:
470 helper, arg = GetGerrit(opts, arg)
471 helper.SetTopic(arg, topic, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800472UserActTopic.usage = '<topic> <CLs...>'
473
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800474
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700475def UserActPrivate(opts, cl, private_str):
Harry Cutts26076b32019-02-26 15:01:29 -0800476 """Set the private bit on a CL to private"""
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700477 try:
478 private = cros_build_lib.BooleanShellValue(private_str, False)
479 except ValueError:
480 raise RuntimeError('Unknown "boolean" value: %s' % private_str)
481
482 helper, cl = GetGerrit(opts, cl)
483 helper.SetPrivate(cl, private)
Harry Cutts26076b32019-02-26 15:01:29 -0800484UserActPrivate.usage = '<CL> <private str>'
Prathmesh Prabhu871e7772018-03-28 17:11:29 -0700485
Mathieu Olivari02f89b32015-01-09 13:53:38 -0800486
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800487def UserActSethashtags(opts, cl, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800488 """Add/remove hashtags on a CL (prepend with '~' to remove)"""
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800489 hashtags = args
490 add = []
491 remove = []
492 for hashtag in hashtags:
493 if hashtag.startswith('~'):
494 remove.append(hashtag[1:])
495 else:
496 add.append(hashtag)
497 helper, cl = GetGerrit(opts, cl)
498 helper.SetHashtags(cl, add, remove, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800499UserActSethashtags.usage = '<CL> <hashtags...>'
Wei-Han Chenb4c9af52017-02-09 14:43:22 +0800500
501
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700502def UserActDeletedraft(opts, *args):
Harry Cutts26076b32019-02-26 15:01:29 -0800503 """Delete draft CLs"""
Mike Frysinger88f27292014-06-17 09:40:45 -0700504 for arg in args:
505 helper, cl = GetGerrit(opts, arg)
506 helper.DeleteDraft(cl, dryrun=opts.dryrun)
Harry Cutts26076b32019-02-26 15:01:29 -0800507UserActDeletedraft.usage = '<CLs...>'
Jon Salza427fb02014-03-07 18:13:17 +0800508
509
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800510def UserActAccount(opts):
Harry Cutts26076b32019-02-26 15:01:29 -0800511 """Get the current user account information"""
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800512 helper, _ = GetGerrit(opts)
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400513 acct = helper.GetAccount()
514 if opts.json:
515 json.dump(acct, sys.stdout)
516 else:
517 print('account_id:%i %s <%s>' %
518 (acct['_account_id'], acct['name'], acct['email']))
Yu-Ju Hongc20d7b32014-11-18 07:51:11 -0800519
520
Harry Cutts26076b32019-02-26 15:01:29 -0800521def _GetActionUsages():
522 """Formats a one-line usage and doc message for each action."""
523 actions = [x for x in globals() if x.startswith(ACTION_PREFIX)]
524 actions.sort()
525
526 cmds = [x[len(ACTION_PREFIX):] for x in actions]
527
528 # Sanity check names for devs adding new commands. Should be quick.
529 for cmd in cmds:
530 expected_name = cmd.lower().capitalize()
531 if cmd != expected_name:
532 raise RuntimeError('callback "%s" is misnamed; should be "%s"' %
533 (cmd, expected_name))
534
535 functions = [globals()[x] for x in actions]
536 usages = [getattr(x, 'usage', '') for x in functions]
537 docs = [x.__doc__ for x in functions]
538
539 action_usages = []
540 cmd_indent = len(max(cmds, key=len))
541 usage_indent = len(max(usages, key=len))
542 for cmd, usage, doc in zip(cmds, usages, docs):
543 action_usages.append(' %-*s %-*s : %s' %
544 (cmd_indent, cmd.lower(), usage_indent, usage, doc))
545
546 return '\n'.join(action_usages)
547
548
Mike Frysinger108eda22018-06-06 18:45:12 -0400549def GetParser():
550 """Returns the parser to use for this module."""
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500551 usage = """%(prog)s [options] <action> [action args]
Mike Frysinger13f23a42013-05-13 17:32:01 -0400552
553There is no support for doing line-by-line code review via the command line.
554This helps you manage various bits and CL status.
555
Mike Frysingera1db2c42014-06-15 00:42:48 -0700556For general Gerrit documentation, see:
557 https://gerrit-review.googlesource.com/Documentation/
558The Searching Changes page covers the search query syntax:
559 https://gerrit-review.googlesource.com/Documentation/user-search.html
560
Mike Frysinger13f23a42013-05-13 17:32:01 -0400561Example:
562 $ gerrit todo # List all the CLs that await your review.
563 $ gerrit mine # List all of your open CLs.
564 $ gerrit inspect 28123 # Inspect CL 28123 on the public gerrit.
565 $ gerrit inspect *28123 # Inspect CL 28123 on the internal gerrit.
566 $ gerrit verify 28123 1 # Mark CL 28123 as verified (+1).
Harry Cuttsde9b32c2019-02-21 15:25:35 -0800567 $ gerrit reviewers 28123 foo@chromium.org # Add foo@ as a reviewer on CL \
56828123.
569 $ gerrit reviewers 28123 ~foo@chromium.org # Remove foo@ as a reviewer on \
570CL 28123.
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700571Scripting:
Mike Frysinger88f27292014-06-17 09:40:45 -0700572 $ gerrit ready `gerrit --raw mine` 1 # Mark *ALL* of your public CLs \
573ready.
574 $ gerrit ready `gerrit --raw -i mine` 1 # Mark *ALL* of your internal CLs \
575ready.
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400576 $ gerrit --json search 'assignee:self' # Dump all pending CLs in JSON.
Mike Frysinger13f23a42013-05-13 17:32:01 -0400577
Harry Cutts26076b32019-02-26 15:01:29 -0800578Actions:
579"""
580 usage += _GetActionUsages()
Mike Frysinger13f23a42013-05-13 17:32:01 -0400581
Alex Klein2ab29cc2018-07-19 12:01:00 -0600582 site_params = config_lib.GetSiteParams()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500583 parser = commandline.ArgumentParser(usage=usage)
Mike Frysinger08737512014-02-07 22:58:26 -0500584 parser.add_argument('-i', '--internal', dest='gob', action='store_const',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600585 default=site_params.EXTERNAL_GOB_INSTANCE,
586 const=site_params.INTERNAL_GOB_INSTANCE,
Mike Frysinger08737512014-02-07 22:58:26 -0500587 help='Query internal Chromium Gerrit instance')
588 parser.add_argument('-g', '--gob',
Alex Klein2ab29cc2018-07-19 12:01:00 -0600589 default=site_params.EXTERNAL_GOB_INSTANCE,
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500590 help=('Gerrit (on borg) instance to query (default: %s)' %
Alex Klein2ab29cc2018-07-19 12:01:00 -0600591 (site_params.EXTERNAL_GOB_INSTANCE)))
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500592 parser.add_argument('--sort', default='number',
Mike Frysingerb62313a2017-06-30 16:38:58 -0400593 help='Key to sort on (number, project); use "unsorted" '
594 'to disable')
Mike Frysingerf70bdc72014-06-15 00:44:06 -0700595 parser.add_argument('--raw', default=False, action='store_true',
596 help='Return raw results (suitable for scripting)')
Mike Frysinger87c74ce2017-04-04 16:12:31 -0400597 parser.add_argument('--json', default=False, action='store_true',
598 help='Return results in JSON (suitable for scripting)')
Mike Frysinger550d9aa2014-06-15 00:55:31 -0700599 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
600 dest='dryrun',
601 help='Show what would be done, but do not make changes')
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800602 parser.add_argument('--ne', '--no-emails', default=True, action='store_false',
603 dest='send_email',
604 help='Do not send email for some operations '
605 '(e.g. ready/review/trybotready/verify)')
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500606 parser.add_argument('-v', '--verbose', default=False, action='store_true',
607 help='Be more verbose in output')
Vadim Bendebury6e057b32014-12-29 09:41:36 -0800608 parser.add_argument('-b', '--branch',
609 help='Limit output to the specific branch')
Vadim Bendebury0278a7e2015-09-05 15:23:13 -0700610 parser.add_argument('--draft', default=False, action='store_true',
611 help="Show draft changes (applicable to 'mine' only)")
Mathieu Olivariedc45b82015-01-12 19:43:20 -0800612 parser.add_argument('-p', '--project',
613 help='Limit output to the specific project')
Mathieu Olivari14645a12015-01-16 15:41:32 -0800614 parser.add_argument('-t', '--topic',
615 help='Limit output to the specific topic')
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500616 parser.add_argument('action', help='The gerrit action to perform')
617 parser.add_argument('args', nargs='*', help='Action arguments')
Mike Frysinger108eda22018-06-06 18:45:12 -0400618
619 return parser
620
621
622def main(argv):
623 parser = GetParser()
Mike Frysingerddf86eb2014-02-07 22:51:41 -0500624 opts = parser.parse_args(argv)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400625
Mike Frysinger88f27292014-06-17 09:40:45 -0700626 # A cache of gerrit helpers we'll load on demand.
627 opts.gerrit = {}
Vadim Bendebury2e3f82d2019-02-11 17:53:03 -0800628
629 # Convert user friendly command line option into a gerrit parameter.
630 opts.notify = 'ALL' if opts.send_email else 'NONE'
Mike Frysinger88f27292014-06-17 09:40:45 -0700631 opts.Freeze()
632
Mike Frysinger27e21b72018-07-12 14:20:21 -0400633 # pylint: disable=global-statement
Mike Frysinger031ad0b2013-05-14 18:15:34 -0400634 global COLOR
635 COLOR = terminal.Color(enabled=opts.color)
636
Mike Frysinger13f23a42013-05-13 17:32:01 -0400637 # Now look up the requested user action and run it.
Mike Frysinger108eda22018-06-06 18:45:12 -0400638 functor = globals().get(ACTION_PREFIX + opts.action.capitalize())
Mike Frysinger13f23a42013-05-13 17:32:01 -0400639 if functor:
640 argspec = inspect.getargspec(functor)
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700641 if argspec.varargs:
Mike Frysingerd8f841c2014-06-15 00:48:26 -0700642 arg_min = getattr(functor, 'arg_min', len(argspec.args))
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500643 if len(opts.args) < arg_min:
Vadim Bendeburydcfe2322013-05-23 10:54:49 -0700644 parser.error('incorrect number of args: %s expects at least %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500645 (opts.action, arg_min))
646 elif len(argspec.args) - 1 != len(opts.args):
Mike Frysinger13f23a42013-05-13 17:32:01 -0400647 parser.error('incorrect number of args: %s expects %s' %
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500648 (opts.action, len(argspec.args) - 1))
Vadim Bendebury614f8682013-05-23 10:33:35 -0700649 try:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500650 functor(opts, *opts.args)
Mike Frysingerc85d8162014-02-08 00:45:21 -0500651 except (cros_build_lib.RunCommandError, gerrit.GerritException,
652 gob_util.GOBError) as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -0400653 cros_build_lib.Die(e)
Mike Frysinger13f23a42013-05-13 17:32:01 -0400654 else:
Mike Frysinger0805c4a2017-02-14 17:38:17 -0500655 parser.error('unknown action: %s' % (opts.action,))