gerrit_client.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. #!/usr/bin/env vpython3
  2. # 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. """Simple client for the Gerrit REST API.
  6. Example usage:
  7. ./gerrit_client.py [command] [args]
  8. """
  9. from __future__ import print_function
  10. import json
  11. import logging
  12. import optparse
  13. import subcommand
  14. import sys
  15. if sys.version_info.major == 2:
  16. import urlparse
  17. from urllib import quote_plus
  18. else:
  19. from urllib.parse import quote_plus
  20. import urllib.parse as urlparse
  21. import fix_encoding
  22. import gerrit_util
  23. import setup_color
  24. __version__ = '0.1'
  25. def write_result(result, opt):
  26. if opt.json_file:
  27. with open(opt.json_file, 'w') as json_file:
  28. json_file.write(json.dumps(result))
  29. @subcommand.usage('[args ...]')
  30. def CMDmovechanges(parser, args):
  31. """Move changes to a different destination branch."""
  32. parser.add_option('-p', '--param', dest='params', action='append',
  33. help='repeatable query parameter, format: -p key=value')
  34. parser.add_option('--destination_branch', dest='destination_branch',
  35. help='where to move changes to')
  36. (opt, args) = parser.parse_args(args)
  37. assert opt.destination_branch, "--destination_branch not defined"
  38. for p in opt.params:
  39. assert '=' in p, '--param is key=value, not "%s"' % p
  40. host = urlparse.urlparse(opt.host).netloc
  41. limit = 100
  42. while True:
  43. result = gerrit_util.QueryChanges(
  44. host,
  45. list(tuple(p.split('=', 1)) for p in opt.params),
  46. limit=limit,
  47. )
  48. for change in result:
  49. gerrit_util.MoveChange(host, change['id'], opt.destination_branch)
  50. if len(result) < limit:
  51. break
  52. logging.info("Done")
  53. @subcommand.usage('[args ...]')
  54. def CMDbranchinfo(parser, args):
  55. """Get information on a gerrit branch."""
  56. parser.add_option('--branch', dest='branch', help='branch name')
  57. (opt, args) = parser.parse_args(args)
  58. host = urlparse.urlparse(opt.host).netloc
  59. project = quote_plus(opt.project)
  60. branch = quote_plus(opt.branch)
  61. result = gerrit_util.GetGerritBranch(host, project, branch)
  62. logging.info(result)
  63. write_result(result, opt)
  64. @subcommand.usage('[args ...]')
  65. def CMDrawapi(parser, args):
  66. """Call an arbitrary Gerrit REST API endpoint."""
  67. parser.add_option('--path', dest='path', help='HTTP path of the API endpoint')
  68. parser.add_option('--method', dest='method',
  69. help='HTTP method for the API (default: GET)')
  70. parser.add_option('--body', dest='body', help='API JSON body contents')
  71. parser.add_option('--accept_status',
  72. dest='accept_status',
  73. help='Comma-delimited list of status codes for success.')
  74. (opt, args) = parser.parse_args(args)
  75. assert opt.path, "--path not defined"
  76. host = urlparse.urlparse(opt.host).netloc
  77. kwargs = {}
  78. if opt.method:
  79. kwargs['reqtype'] = opt.method.upper()
  80. if opt.body:
  81. kwargs['body'] = json.loads(opt.body)
  82. if opt.accept_status:
  83. kwargs['accept_statuses'] = [int(x) for x in opt.accept_status.split(',')]
  84. result = gerrit_util.CallGerritApi(host, opt.path, **kwargs)
  85. logging.info(result)
  86. write_result(result, opt)
  87. @subcommand.usage('[args ...]')
  88. def CMDbranch(parser, args):
  89. """Create a branch in a gerrit project."""
  90. parser.add_option('--branch', dest='branch', help='branch name')
  91. parser.add_option('--commit', dest='commit', help='commit hash')
  92. parser.add_option('--allow-existent-branch',
  93. action='store_true',
  94. help=('Accept that the branch alread exists as long as the'
  95. ' branch head points the given commit'))
  96. (opt, args) = parser.parse_args(args)
  97. assert opt.project, "--project not defined"
  98. assert opt.branch, "--branch not defined"
  99. assert opt.commit, "--commit not defined"
  100. project = quote_plus(opt.project)
  101. host = urlparse.urlparse(opt.host).netloc
  102. branch = quote_plus(opt.branch)
  103. result = gerrit_util.GetGerritBranch(host, project, branch)
  104. if result:
  105. if not opt.allow_existent_branch:
  106. raise gerrit_util.GerritError(200, 'Branch already exists')
  107. if result.get('revision') != opt.commit:
  108. raise gerrit_util.GerritError(
  109. 200, ('Branch already exists but '
  110. 'the branch head is not at the given commit'))
  111. else:
  112. try:
  113. result = gerrit_util.CreateGerritBranch(host, project, branch, opt.commit)
  114. except gerrit_util.GerritError as e:
  115. result = gerrit_util.GetGerritBranch(host, project, branch)
  116. if not result:
  117. raise e
  118. # If reached here, we hit a real conflict error, because the
  119. # branch just created is pointing a different commit.
  120. if result.get('revision') != opt.commit:
  121. raise gerrit_util.GerritError(
  122. 200, ('Conflict: branch was created but '
  123. 'the branch head is not at the given commit'))
  124. logging.info(result)
  125. write_result(result, opt)
  126. @subcommand.usage('[args ...]')
  127. def CMDtag(parser, args):
  128. """Create a tag in a gerrit project."""
  129. parser.add_option('--tag', dest='tag', help='tag name')
  130. parser.add_option('--commit', dest='commit', help='commit hash')
  131. (opt, args) = parser.parse_args(args)
  132. assert opt.project, "--project not defined"
  133. assert opt.tag, "--tag not defined"
  134. assert opt.commit, "--commit not defined"
  135. project = quote_plus(opt.project)
  136. host = urlparse.urlparse(opt.host).netloc
  137. tag = quote_plus(opt.tag)
  138. result = gerrit_util.CreateGerritTag(host, project, tag, opt.commit)
  139. logging.info(result)
  140. write_result(result, opt)
  141. @subcommand.usage('[args ...]')
  142. def CMDhead(parser, args):
  143. """Update which branch the project HEAD points to."""
  144. parser.add_option('--branch', dest='branch', help='branch name')
  145. (opt, args) = parser.parse_args(args)
  146. assert opt.project, "--project not defined"
  147. assert opt.branch, "--branch not defined"
  148. project = quote_plus(opt.project)
  149. host = urlparse.urlparse(opt.host).netloc
  150. branch = quote_plus(opt.branch)
  151. result = gerrit_util.UpdateHead(host, project, branch)
  152. logging.info(result)
  153. write_result(result, opt)
  154. @subcommand.usage('[args ...]')
  155. def CMDheadinfo(parser, args):
  156. """Retrieves the current HEAD of the project."""
  157. (opt, args) = parser.parse_args(args)
  158. assert opt.project, "--project not defined"
  159. project = quote_plus(opt.project)
  160. host = urlparse.urlparse(opt.host).netloc
  161. result = gerrit_util.GetHead(host, project)
  162. logging.info(result)
  163. write_result(result, opt)
  164. @subcommand.usage('[args ...]')
  165. def CMDchanges(parser, args):
  166. """Queries gerrit for matching changes."""
  167. parser.add_option('-p',
  168. '--param',
  169. dest='params',
  170. action='append',
  171. default=[],
  172. help='repeatable query parameter, format: -p key=value')
  173. parser.add_option('--query', help='raw gerrit search query string')
  174. parser.add_option('-o', '--o-param', dest='o_params', action='append',
  175. help='gerrit output parameters, e.g. ALL_REVISIONS')
  176. parser.add_option('--limit', dest='limit', type=int,
  177. help='maximum number of results to return')
  178. parser.add_option('--start', dest='start', type=int,
  179. help='how many changes to skip '
  180. '(starting with the most recent)')
  181. (opt, args) = parser.parse_args(args)
  182. assert opt.params or opt.query, '--param or --query required'
  183. for p in opt.params:
  184. assert '=' in p, '--param is key=value, not "%s"' % p
  185. result = gerrit_util.QueryChanges(
  186. urlparse.urlparse(opt.host).netloc,
  187. list(tuple(p.split('=', 1)) for p in opt.params),
  188. first_param=opt.query,
  189. start=opt.start, # Default: None
  190. limit=opt.limit, # Default: None
  191. o_params=opt.o_params, # Default: None
  192. )
  193. logging.info('Change query returned %d changes.', len(result))
  194. write_result(result, opt)
  195. @subcommand.usage('[args ...]')
  196. def CMDrelatedchanges(parser, args):
  197. """Gets related changes for a given change and revision."""
  198. parser.add_option('-c', '--change', type=str, help='change id')
  199. parser.add_option('-r', '--revision', type=str, help='revision id')
  200. (opt, args) = parser.parse_args(args)
  201. result = gerrit_util.GetRelatedChanges(
  202. urlparse.urlparse(opt.host).netloc,
  203. change=opt.change,
  204. revision=opt.revision,
  205. )
  206. logging.info(result)
  207. write_result(result, opt)
  208. @subcommand.usage('[args ...]')
  209. def CMDcreatechange(parser, args):
  210. """Create a new change in gerrit."""
  211. parser.add_option('-s', '--subject', help='subject for change')
  212. parser.add_option('-b',
  213. '--branch',
  214. default='main',
  215. help='target branch for change')
  216. parser.add_option(
  217. '-p',
  218. '--param',
  219. dest='params',
  220. action='append',
  221. help='repeatable field value parameter, format: -p key=value')
  222. parser.add_option('--cc',
  223. dest='cc_list',
  224. action='append',
  225. help='CC address to notify, format: --cc foo@example.com')
  226. (opt, args) = parser.parse_args(args)
  227. for p in opt.params:
  228. assert '=' in p, '--param is key=value, not "%s"' % p
  229. params = list(tuple(p.split('=', 1)) for p in opt.params)
  230. if opt.cc_list:
  231. params.append(('notify_details', {'CC': {'accounts': opt.cc_list}}))
  232. result = gerrit_util.CreateChange(
  233. urlparse.urlparse(opt.host).netloc,
  234. opt.project,
  235. branch=opt.branch,
  236. subject=opt.subject,
  237. params=params,
  238. )
  239. logging.info(result)
  240. write_result(result, opt)
  241. @subcommand.usage('[args ...]')
  242. def CMDchangeedit(parser, args):
  243. """Puts content of a file into a change edit."""
  244. parser.add_option('-c', '--change', type=int, help='change number')
  245. parser.add_option('--path', help='path for file')
  246. parser.add_option('--file', help='file to place at |path|')
  247. (opt, args) = parser.parse_args(args)
  248. with open(opt.file) as f:
  249. data = f.read()
  250. result = gerrit_util.ChangeEdit(
  251. urlparse.urlparse(opt.host).netloc, opt.change, opt.path, data)
  252. logging.info(result)
  253. write_result(result, opt)
  254. @subcommand.usage('[args ...]')
  255. def CMDpublishchangeedit(parser, args):
  256. """Publish a Gerrit change edit."""
  257. parser.add_option('-c', '--change', type=int, help='change number')
  258. parser.add_option('--notify', help='whether to notify')
  259. (opt, args) = parser.parse_args(args)
  260. result = gerrit_util.PublishChangeEdit(
  261. urlparse.urlparse(opt.host).netloc, opt.change, opt.notify)
  262. logging.info(result)
  263. write_result(result, opt)
  264. @subcommand.usage('[args ...]')
  265. def CMDsubmitchange(parser, args):
  266. """Submit a Gerrit change."""
  267. parser.add_option('-c', '--change', type=int, help='change number')
  268. (opt, args) = parser.parse_args(args)
  269. result = gerrit_util.SubmitChange(
  270. urlparse.urlparse(opt.host).netloc, opt.change)
  271. logging.info(result)
  272. write_result(result, opt)
  273. @subcommand.usage('[args ...]')
  274. def CMDchangesubmittedtogether(parser, args):
  275. """Get all changes submitted with the given one."""
  276. parser.add_option('-c', '--change', type=int, help='change number')
  277. (opt, args) = parser.parse_args(args)
  278. result = gerrit_util.GetChangesSubmittedTogether(
  279. urlparse.urlparse(opt.host).netloc, opt.change)
  280. logging.info(result)
  281. write_result(result, opt)
  282. @subcommand.usage('[args ...]')
  283. def CMDgetcommitincludedin(parser, args):
  284. """Retrieves the branches and tags for a given commit."""
  285. parser.add_option('--commit', dest='commit', help='commit hash')
  286. (opt, args) = parser.parse_args(args)
  287. result = gerrit_util.GetCommitIncludedIn(
  288. urlparse.urlparse(opt.host).netloc, opt.project, opt.commit)
  289. logging.info(result)
  290. write_result(result, opt)
  291. @subcommand.usage('[args ...]')
  292. def CMDsetbotcommit(parser, args):
  293. """Sets bot-commit+1 to a bot generated change."""
  294. parser.add_option('-c', '--change', type=int, help='change number')
  295. (opt, args) = parser.parse_args(args)
  296. result = gerrit_util.SetReview(
  297. urlparse.urlparse(opt.host).netloc,
  298. opt.change,
  299. labels={'Bot-Commit': 1},
  300. ready=True)
  301. logging.info(result)
  302. write_result(result, opt)
  303. @subcommand.usage('[args ...]')
  304. def CMDsetlabel(parser, args):
  305. """Sets a label to a specific value on a given change."""
  306. parser.add_option('-c', '--change', type=int, help='change number')
  307. parser.add_option('-l',
  308. '--label',
  309. nargs=2,
  310. metavar=('label_name', 'label_value'))
  311. (opt, args) = parser.parse_args(args)
  312. result = gerrit_util.SetReview(urlparse.urlparse(opt.host).netloc,
  313. opt.change,
  314. labels={opt.label[0]: opt.label[1]})
  315. logging.info(result)
  316. write_result(result, opt)
  317. @subcommand.usage('')
  318. def CMDabandon(parser, args):
  319. """Abandons a Gerrit change."""
  320. parser.add_option('-c', '--change', type=int, help='change number')
  321. parser.add_option('-m', '--message', default='', help='reason for abandoning')
  322. (opt, args) = parser.parse_args(args)
  323. assert opt.change, "-c not defined"
  324. result = gerrit_util.AbandonChange(
  325. urlparse.urlparse(opt.host).netloc,
  326. opt.change, opt.message)
  327. logging.info(result)
  328. write_result(result, opt)
  329. @subcommand.usage('')
  330. def CMDmass_abandon(parser, args):
  331. """Mass abandon changes
  332. Abandons CLs that match search criteria provided by user. Before any change is
  333. actually abandoned, user is presented with a list of CLs that will be affected
  334. if user confirms. User can skip confirmation by passing --force parameter.
  335. The script can abandon up to 100 CLs per invocation.
  336. Examples:
  337. gerrit_client.py mass-abandon --host https://HOST -p 'project=repo2'
  338. gerrit_client.py mass-abandon --host https://HOST -p 'message=testing'
  339. gerrit_client.py mass-abandon --host https://HOST -p 'is=wip' -p 'age=1y'
  340. """
  341. parser.add_option('-p',
  342. '--param',
  343. dest='params',
  344. action='append',
  345. default=[],
  346. help='repeatable query parameter, format: -p key=value')
  347. parser.add_option('-m', '--message', default='', help='reason for abandoning')
  348. parser.add_option('-f',
  349. '--force',
  350. action='store_true',
  351. help='Don\'t prompt for confirmation')
  352. opt, args = parser.parse_args(args)
  353. for p in opt.params:
  354. assert '=' in p, '--param is key=value, not "%s"' % p
  355. search_query = list(tuple(p.split('=', 1)) for p in opt.params)
  356. if not any(t for t in search_query if t[0] == 'owner'):
  357. # owner should always be present when abandoning changes
  358. search_query.append(('owner', 'me'))
  359. search_query.append(('status', 'open'))
  360. logging.info("Searching for: %s" % search_query)
  361. host = urlparse.urlparse(opt.host).netloc
  362. result = gerrit_util.QueryChanges(
  363. host,
  364. search_query,
  365. # abandon at most 100 changes as not all Gerrit instances support
  366. # unlimited results.
  367. limit=100,
  368. )
  369. if len(result) == 0:
  370. logging.warning("Nothing to abandon")
  371. return
  372. logging.warning("%s CLs match search query: " % len(result))
  373. for change in result:
  374. logging.warning("[ID: %d] %s" % (change['_number'], change['subject']))
  375. if not opt.force:
  376. q = input(
  377. 'Do you want to move forward with abandoning? [y to confirm] ').strip()
  378. if q not in ['y', 'Y']:
  379. logging.warning("Aborting...")
  380. return
  381. for change in result:
  382. logging.warning("Abandoning: %s" % change['subject'])
  383. gerrit_util.AbandonChange(host, change['id'], opt.message)
  384. logging.warning("Done")
  385. class OptionParser(optparse.OptionParser):
  386. """Creates the option parse and add --verbose support."""
  387. def __init__(self, *args, **kwargs):
  388. optparse.OptionParser.__init__(self, *args, version=__version__, **kwargs)
  389. self.add_option(
  390. '--verbose', action='count', default=0,
  391. help='Use 2 times for more debugging info')
  392. self.add_option('--host', dest='host', help='Url of host.')
  393. self.add_option('--project', dest='project', help='project name')
  394. self.add_option(
  395. '--json_file', dest='json_file', help='output json filepath')
  396. def parse_args(self, args=None, values=None):
  397. options, args = optparse.OptionParser.parse_args(self, args, values)
  398. # Host is always required
  399. assert options.host, "--host not defined."
  400. levels = [logging.WARNING, logging.INFO, logging.DEBUG]
  401. logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
  402. return options, args
  403. def main(argv):
  404. if sys.hexversion < 0x02060000:
  405. print('\nYour python version %s is unsupported, please upgrade.\n'
  406. % (sys.version.split(' ', 1)[0],),
  407. file=sys.stderr)
  408. return 2
  409. dispatcher = subcommand.CommandDispatcher(__name__)
  410. return dispatcher.execute(OptionParser(), argv)
  411. if __name__ == '__main__':
  412. # These affect sys.stdout so do it outside of main() to simplify mocks in
  413. # unit testing.
  414. fix_encoding.fix_encoding()
  415. setup_color.init()
  416. try:
  417. sys.exit(main(sys.argv[1:]))
  418. except KeyboardInterrupt:
  419. sys.stderr.write('interrupted\n')
  420. sys.exit(1)