blob: a4ed70ccf9b2a2e67ba96ddba4c4c21326e7e876 [file] [log] [blame]
Josip Sokcevic84434e82021-06-09 22:48:43 +00001#!/usr/bin/env vpython3
dimu833c94c2017-01-18 17:36:15 -08002# Copyright 2017 The Chromium 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
6"""Simple client for the Gerrit REST API.
7
8Example usage:
Michael Moss5eebf6f2021-07-16 13:18:34 +00009 ./gerrit_client.py [command] [args]
dimu833c94c2017-01-18 17:36:15 -080010"""
11
12from __future__ import print_function
13
14import json
15import logging
16import optparse
17import subcommand
18import sys
Josip Sokcevicc99efb22020-03-17 00:35:34 +000019
20if sys.version_info.major == 2:
21 import urlparse
22 from urllib import quote_plus
23else:
24 from urllib.parse import quote_plus
25 import urllib.parse as urlparse
dimu833c94c2017-01-18 17:36:15 -080026
dimu833c94c2017-01-18 17:36:15 -080027import fix_encoding
28import gerrit_util
29import setup_color
30
31__version__ = '0.1'
dimu833c94c2017-01-18 17:36:15 -080032
33
34def write_result(result, opt):
35 if opt.json_file:
36 with open(opt.json_file, 'w') as json_file:
37 json_file.write(json.dumps(result))
38
39
40@subcommand.usage('[args ...]')
Josip Sokcevicc39ab992020-09-24 20:09:15 +000041def CMDmovechanges(parser, args):
Michael Moss5eebf6f2021-07-16 13:18:34 +000042 """Move changes to a different destination branch."""
Josip Sokcevicc39ab992020-09-24 20:09:15 +000043 parser.add_option('-p', '--param', dest='params', action='append',
44 help='repeatable query parameter, format: -p key=value')
45 parser.add_option('--destination_branch', dest='destination_branch',
46 help='where to move changes to')
47
48 (opt, args) = parser.parse_args(args)
49 assert opt.destination_branch, "--destination_branch not defined"
Mike Frysinger8820ab82020-11-25 00:52:31 +000050 for p in opt.params:
51 assert '=' in p, '--param is key=value, not "%s"' % p
Josip Sokcevicc39ab992020-09-24 20:09:15 +000052 host = urlparse.urlparse(opt.host).netloc
53
54 limit = 100
55 while True:
56 result = gerrit_util.QueryChanges(
57 host,
58 list(tuple(p.split('=', 1)) for p in opt.params),
59 limit=limit,
60 )
61 for change in result:
62 gerrit_util.MoveChange(host, change['id'], opt.destination_branch)
63
64 if len(result) < limit:
65 break
66 logging.info("Done")
67
68
69@subcommand.usage('[args ...]')
dimu833c94c2017-01-18 17:36:15 -080070def CMDbranchinfo(parser, args):
Michael Moss5eebf6f2021-07-16 13:18:34 +000071 """Get information on a gerrit branch."""
dimu833c94c2017-01-18 17:36:15 -080072 parser.add_option('--branch', dest='branch', help='branch name')
73
74 (opt, args) = parser.parse_args(args)
75 host = urlparse.urlparse(opt.host).netloc
Josip Sokcevicc99efb22020-03-17 00:35:34 +000076 project = quote_plus(opt.project)
77 branch = quote_plus(opt.branch)
dimu833c94c2017-01-18 17:36:15 -080078 result = gerrit_util.GetGerritBranch(host, project, branch)
79 logging.info(result)
80 write_result(result, opt)
81
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000082
dimu833c94c2017-01-18 17:36:15 -080083@subcommand.usage('[args ...]')
84def CMDbranch(parser, args):
Michael Moss5eebf6f2021-07-16 13:18:34 +000085 """Create a branch in a gerrit project."""
dimu833c94c2017-01-18 17:36:15 -080086 parser.add_option('--branch', dest='branch', help='branch name')
87 parser.add_option('--commit', dest='commit', help='commit hash')
88
89 (opt, args) = parser.parse_args(args)
Josip Sokcevicc99efb22020-03-17 00:35:34 +000090 assert opt.project, "--project not defined"
91 assert opt.branch, "--branch not defined"
92 assert opt.commit, "--commit not defined"
dimu833c94c2017-01-18 17:36:15 -080093
Josip Sokcevicc99efb22020-03-17 00:35:34 +000094 project = quote_plus(opt.project)
dimu833c94c2017-01-18 17:36:15 -080095 host = urlparse.urlparse(opt.host).netloc
Josip Sokcevicc99efb22020-03-17 00:35:34 +000096 branch = quote_plus(opt.branch)
97 commit = quote_plus(opt.commit)
dimu833c94c2017-01-18 17:36:15 -080098 result = gerrit_util.CreateGerritBranch(host, project, branch, commit)
99 logging.info(result)
100 write_result(result, opt)
101
102
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200103@subcommand.usage('[args ...]')
Josip Sokcevicdf9a8022020-12-08 00:10:19 +0000104def CMDhead(parser, args):
Michael Moss5eebf6f2021-07-16 13:18:34 +0000105 """Update which branch the project HEAD points to."""
Josip Sokcevicdf9a8022020-12-08 00:10:19 +0000106 parser.add_option('--branch', dest='branch', help='branch name')
107
108 (opt, args) = parser.parse_args(args)
109 assert opt.project, "--project not defined"
110 assert opt.branch, "--branch not defined"
111
112 project = quote_plus(opt.project)
113 host = urlparse.urlparse(opt.host).netloc
114 branch = quote_plus(opt.branch)
115 result = gerrit_util.UpdateHead(host, project, branch)
116 logging.info(result)
117 write_result(result, opt)
118
119
120@subcommand.usage('[args ...]')
121def CMDheadinfo(parser, args):
Michael Moss5eebf6f2021-07-16 13:18:34 +0000122 """Retrieves the current HEAD of the project."""
Josip Sokcevicdf9a8022020-12-08 00:10:19 +0000123
124 (opt, args) = parser.parse_args(args)
125 assert opt.project, "--project not defined"
126
127 project = quote_plus(opt.project)
128 host = urlparse.urlparse(opt.host).netloc
129 result = gerrit_util.GetHead(host, project)
130 logging.info(result)
131 write_result(result, opt)
132
133
134@subcommand.usage('[args ...]')
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200135def CMDchanges(parser, args):
Michael Moss5eebf6f2021-07-16 13:18:34 +0000136 """Queries gerrit for matching changes."""
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200137 parser.add_option('-p', '--param', dest='params', action='append',
138 help='repeatable query parameter, format: -p key=value')
Paweł Hajdan, Jr24025d32017-07-11 16:38:21 +0200139 parser.add_option('-o', '--o-param', dest='o_params', action='append',
140 help='gerrit output parameters, e.g. ALL_REVISIONS')
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200141 parser.add_option('--limit', dest='limit', type=int,
142 help='maximum number of results to return')
143 parser.add_option('--start', dest='start', type=int,
144 help='how many changes to skip '
145 '(starting with the most recent)')
146
147 (opt, args) = parser.parse_args(args)
Mike Frysinger8820ab82020-11-25 00:52:31 +0000148 for p in opt.params:
149 assert '=' in p, '--param is key=value, not "%s"' % p
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200150
151 result = gerrit_util.QueryChanges(
152 urlparse.urlparse(opt.host).netloc,
153 list(tuple(p.split('=', 1)) for p in opt.params),
Paweł Hajdan, Jr24025d32017-07-11 16:38:21 +0200154 start=opt.start, # Default: None
155 limit=opt.limit, # Default: None
156 o_params=opt.o_params, # Default: None
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200157 )
158 logging.info('Change query returned %d changes.', len(result))
159 write_result(result, opt)
160
161
LaMont Jones9eed4232021-04-02 16:29:49 +0000162@subcommand.usage('[args ...]')
Marco Georgaklis85557a02021-06-03 15:56:54 +0000163def CMDrelatedchanges(parser, args):
Michael Moss5eebf6f2021-07-16 13:18:34 +0000164 """Gets related changes for a given change and revision."""
Marco Georgaklis85557a02021-06-03 15:56:54 +0000165 parser.add_option('-c', '--change', type=str, help='change id')
166 parser.add_option('-r', '--revision', type=str, help='revision id')
167
168 (opt, args) = parser.parse_args(args)
169
170 result = gerrit_util.GetRelatedChanges(
171 urlparse.urlparse(opt.host).netloc,
172 change=opt.change,
173 revision=opt.revision,
174 )
175 logging.info(result)
176 write_result(result, opt)
177
178
179@subcommand.usage('[args ...]')
LaMont Jones9eed4232021-04-02 16:29:49 +0000180def CMDcreatechange(parser, args):
Michael Moss5eebf6f2021-07-16 13:18:34 +0000181 """Create a new change in gerrit."""
LaMont Jones9eed4232021-04-02 16:29:49 +0000182 parser.add_option('-s', '--subject', help='subject for change')
183 parser.add_option('-b',
184 '--branch',
185 default='main',
186 help='target branch for change')
187 parser.add_option(
188 '-p',
189 '--param',
190 dest='params',
191 action='append',
192 help='repeatable field value parameter, format: -p key=value')
193
194 (opt, args) = parser.parse_args(args)
195 for p in opt.params:
196 assert '=' in p, '--param is key=value, not "%s"' % p
197
198 result = gerrit_util.CreateChange(
199 urlparse.urlparse(opt.host).netloc,
200 opt.project,
201 branch=opt.branch,
202 subject=opt.subject,
203 params=list(tuple(p.split('=', 1)) for p in opt.params),
204 )
205 logging.info(result)
206 write_result(result, opt)
207
208
209@subcommand.usage('[args ...]')
210def CMDchangeedit(parser, args):
Michael Moss5eebf6f2021-07-16 13:18:34 +0000211 """Puts content of a file into a change edit."""
LaMont Jones9eed4232021-04-02 16:29:49 +0000212 parser.add_option('-c', '--change', type=int, help='change number')
213 parser.add_option('--path', help='path for file')
214 parser.add_option('--file', help='file to place at |path|')
215
216 (opt, args) = parser.parse_args(args)
217
218 with open(opt.file) as f:
219 data = f.read()
220 result = gerrit_util.ChangeEdit(
221 urlparse.urlparse(opt.host).netloc, opt.change, opt.path, data)
222 logging.info(result)
223 write_result(result, opt)
224
225
226@subcommand.usage('[args ...]')
227def CMDpublishchangeedit(parser, args):
Michael Moss5eebf6f2021-07-16 13:18:34 +0000228 """Publish a Gerrit change edit."""
LaMont Jones9eed4232021-04-02 16:29:49 +0000229 parser.add_option('-c', '--change', type=int, help='change number')
230 parser.add_option('--notify', help='whether to notify')
231
232 (opt, args) = parser.parse_args(args)
233
234 result = gerrit_util.PublishChangeEdit(
235 urlparse.urlparse(opt.host).netloc, opt.change, opt.notify)
236 logging.info(result)
237 write_result(result, opt)
238
239
Xinan Lin6ec7cd82021-07-21 00:53:42 +0000240@subcommand.usage('[args ...]')
241def CMDsubmitchange(parser, args):
242 """Submit a Gerrit change."""
243 parser.add_option('-c', '--change', type=int, help='change number')
Xinan Lin6ec7cd82021-07-21 00:53:42 +0000244 (opt, args) = parser.parse_args(args)
Xinan Lin1bd4ffa2021-07-28 00:54:22 +0000245 result = gerrit_util.SubmitChange(
246 urlparse.urlparse(opt.host).netloc, opt.change)
Xinan Lin6ec7cd82021-07-21 00:53:42 +0000247 logging.info(result)
248 write_result(result, opt)
249
250
Xinan Linc2fb26a2021-07-27 18:01:55 +0000251@subcommand.usage('[args ...]')
Xinan Lin2b4ec952021-08-20 17:35:29 +0000252def CMDchangesubmittedtogether(parser, args):
253 """Get all changes submitted with the given one."""
254 parser.add_option('-c', '--change', type=int, help='change number')
255 (opt, args) = parser.parse_args(args)
256 result = gerrit_util.GetChangesSubmittedTogether(
257 urlparse.urlparse(opt.host).netloc, opt.change)
258 logging.info(result)
259 write_result(result, opt)
260
261
262@subcommand.usage('[args ...]')
Xinan Linc2fb26a2021-07-27 18:01:55 +0000263def CMDgetcommitincludedin(parser, args):
264 """Retrieves the branches and tags for a given commit."""
265 parser.add_option('--commit', dest='commit', help='commit hash')
266 (opt, args) = parser.parse_args(args)
267 result = gerrit_util.GetCommitIncludedIn(
268 urlparse.urlparse(opt.host).netloc, opt.project, opt.commit)
269 logging.info(result)
270 write_result(result, opt)
271
272
Xinan Lin0b0738d2021-07-27 19:13:49 +0000273@subcommand.usage('[args ...]')
274def CMDsetbotcommit(parser, args):
275 """Sets bot-commit+1 to a bot generated change."""
276 parser.add_option('-c', '--change', type=int, help='change number')
277 (opt, args) = parser.parse_args(args)
278 result = gerrit_util.SetReview(
279 urlparse.urlparse(opt.host).netloc,
280 opt.change,
281 labels={'Bot-Commit': 1},
282 ready=True)
283 logging.info(result)
284 write_result(result, opt)
285
286
Ben Pastene281edf72021-10-06 01:23:24 +0000287@subcommand.usage('[args ...]')
288def CMDsetlabel(parser, args):
289 """Sets a label to a specific value on a given change."""
290 parser.add_option('-c', '--change', type=int, help='change number')
291 parser.add_option('-l',
292 '--label',
293 nargs=2,
294 metavar=('label_name', 'label_value'))
295 (opt, args) = parser.parse_args(args)
296 result = gerrit_util.SetReview(urlparse.urlparse(opt.host).netloc,
297 opt.change,
298 labels={opt.label[0]: opt.label[1]})
299 logging.info(result)
300 write_result(result, opt)
301
302
Sergiy Belozorovfe347232019-02-27 15:07:33 +0000303@subcommand.usage('')
304def CMDabandon(parser, args):
Michael Moss5eebf6f2021-07-16 13:18:34 +0000305 """Abandons a Gerrit change."""
Sergiy Belozorovfe347232019-02-27 15:07:33 +0000306 parser.add_option('-c', '--change', type=int, help='change number')
307 parser.add_option('-m', '--message', default='', help='reason for abandoning')
308
309 (opt, args) = parser.parse_args(args)
Josip Sokcevicc99efb22020-03-17 00:35:34 +0000310 assert opt.change, "-c not defined"
Sergiy Belozorovfe347232019-02-27 15:07:33 +0000311 result = gerrit_util.AbandonChange(
312 urlparse.urlparse(opt.host).netloc,
313 opt.change, opt.message)
314 logging.info(result)
315 write_result(result, opt)
316
317
Michael Moss5eebf6f2021-07-16 13:18:34 +0000318@subcommand.usage('')
Josip Sokcevice1a98942021-04-07 21:35:29 +0000319def CMDmass_abandon(parser, args):
Michael Moss5eebf6f2021-07-16 13:18:34 +0000320 """Mass abandon changes
321
322 Abandons CLs that match search criteria provided by user. Before any change is
323 actually abandoned, user is presented with a list of CLs that will be affected
324 if user confirms. User can skip confirmation by passing --force parameter.
325
326 The script can abandon up to 100 CLs per invocation.
327
328 Examples:
329 gerrit_client.py mass-abandon --host https://HOST -p 'project=repo2'
330 gerrit_client.py mass-abandon --host https://HOST -p 'message=testing'
331 gerrit_client.py mass-abandon --host https://HOST -p 'is=wip' -p 'age=1y'
332 """
Josip Sokcevice1a98942021-04-07 21:35:29 +0000333 parser.add_option('-p',
334 '--param',
335 dest='params',
336 action='append',
337 default=[],
338 help='repeatable query parameter, format: -p key=value')
339 parser.add_option('-m', '--message', default='', help='reason for abandoning')
340 parser.add_option('-f',
341 '--force',
342 action='store_true',
343 help='Don\'t prompt for confirmation')
344
345 opt, args = parser.parse_args(args)
346
347 for p in opt.params:
348 assert '=' in p, '--param is key=value, not "%s"' % p
349 search_query = list(tuple(p.split('=', 1)) for p in opt.params)
350 if not any(t for t in search_query if t[0] == 'owner'):
351 # owner should always be present when abandoning changes
352 search_query.append(('owner', 'me'))
353 search_query.append(('status', 'open'))
354 logging.info("Searching for: %s" % search_query)
355
356 host = urlparse.urlparse(opt.host).netloc
357
358 result = gerrit_util.QueryChanges(
359 host,
360 search_query,
361 # abandon at most 100 changes as not all Gerrit instances support
362 # unlimited results.
363 limit=100,
364 )
365 if len(result) == 0:
366 logging.warn("Nothing to abandon")
367 return
368
369 logging.warn("%s CLs match search query: " % len(result))
370 for change in result:
371 logging.warn("[ID: %d] %s" % (change['_number'], change['subject']))
372
373 if not opt.force:
374 q = raw_input(
375 'Do you want to move forward with abandoning? [y to confirm] ').strip()
376 if q not in ['y', 'Y']:
377 logging.warn("Aborting...")
378 return
379
380 for change in result:
381 logging.warning("Abandoning: %s" % change['subject'])
382 gerrit_util.AbandonChange(host, change['id'], opt.message)
383
384 logging.warning("Done")
385
386
dimu833c94c2017-01-18 17:36:15 -0800387class OptionParser(optparse.OptionParser):
388 """Creates the option parse and add --verbose support."""
389 def __init__(self, *args, **kwargs):
Josip Sokcevicc99efb22020-03-17 00:35:34 +0000390 optparse.OptionParser.__init__(self, *args, version=__version__, **kwargs)
dimu833c94c2017-01-18 17:36:15 -0800391 self.add_option(
392 '--verbose', action='count', default=0,
393 help='Use 2 times for more debugging info')
394 self.add_option('--host', dest='host', help='Url of host.')
395 self.add_option('--project', dest='project', help='project name')
396 self.add_option(
397 '--json_file', dest='json_file', help='output json filepath')
398
399 def parse_args(self, args=None, values=None):
400 options, args = optparse.OptionParser.parse_args(self, args, values)
Josip Sokcevicc99efb22020-03-17 00:35:34 +0000401 # Host is always required
402 assert options.host, "--host not defined."
dimu833c94c2017-01-18 17:36:15 -0800403 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
404 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
405 return options, args
406
407
408def main(argv):
409 if sys.hexversion < 0x02060000:
410 print('\nYour python version %s is unsupported, please upgrade.\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000411 % (sys.version.split(' ', 1)[0],),
dimu833c94c2017-01-18 17:36:15 -0800412 file=sys.stderr)
413 return 2
414 dispatcher = subcommand.CommandDispatcher(__name__)
415 return dispatcher.execute(OptionParser(), argv)
416
417
418if __name__ == '__main__':
419 # These affect sys.stdout so do it outside of main() to simplify mocks in
420 # unit testing.
421 fix_encoding.fix_encoding()
422 setup_color.init()
423 try:
424 sys.exit(main(sys.argv[1:]))
425 except KeyboardInterrupt:
426 sys.stderr.write('interrupted\n')
Michael Achenbach6fbf12f2017-07-06 10:54:11 +0200427 sys.exit(1)