gerrit_client.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  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. (opt, args) = parser.parse_args(args)
  93. assert opt.project, "--project not defined"
  94. assert opt.branch, "--branch not defined"
  95. assert opt.commit, "--commit not defined"
  96. project = quote_plus(opt.project)
  97. host = urlparse.urlparse(opt.host).netloc
  98. branch = quote_plus(opt.branch)
  99. result = gerrit_util.CreateGerritBranch(host, project, branch, opt.commit)
  100. logging.info(result)
  101. write_result(result, opt)
  102. @subcommand.usage('[args ...]')
  103. def CMDtag(parser, args):
  104. """Create a tag in a gerrit project."""
  105. parser.add_option('--tag', dest='tag', help='tag name')
  106. parser.add_option('--commit', dest='commit', help='commit hash')
  107. (opt, args) = parser.parse_args(args)
  108. assert opt.project, "--project not defined"
  109. assert opt.tag, "--tag not defined"
  110. assert opt.commit, "--commit not defined"
  111. project = quote_plus(opt.project)
  112. host = urlparse.urlparse(opt.host).netloc
  113. tag = quote_plus(opt.tag)
  114. result = gerrit_util.CreateGerritTag(host, project, tag, opt.commit)
  115. logging.info(result)
  116. write_result(result, opt)
  117. @subcommand.usage('[args ...]')
  118. def CMDhead(parser, args):
  119. """Update which branch the project HEAD points to."""
  120. parser.add_option('--branch', dest='branch', help='branch name')
  121. (opt, args) = parser.parse_args(args)
  122. assert opt.project, "--project not defined"
  123. assert opt.branch, "--branch not defined"
  124. project = quote_plus(opt.project)
  125. host = urlparse.urlparse(opt.host).netloc
  126. branch = quote_plus(opt.branch)
  127. result = gerrit_util.UpdateHead(host, project, branch)
  128. logging.info(result)
  129. write_result(result, opt)
  130. @subcommand.usage('[args ...]')
  131. def CMDheadinfo(parser, args):
  132. """Retrieves the current HEAD of the project."""
  133. (opt, args) = parser.parse_args(args)
  134. assert opt.project, "--project not defined"
  135. project = quote_plus(opt.project)
  136. host = urlparse.urlparse(opt.host).netloc
  137. result = gerrit_util.GetHead(host, project)
  138. logging.info(result)
  139. write_result(result, opt)
  140. @subcommand.usage('[args ...]')
  141. def CMDchanges(parser, args):
  142. """Queries gerrit for matching changes."""
  143. parser.add_option('-p', '--param', dest='params', action='append',
  144. help='repeatable query parameter, format: -p key=value')
  145. parser.add_option('-o', '--o-param', dest='o_params', action='append',
  146. help='gerrit output parameters, e.g. ALL_REVISIONS')
  147. parser.add_option('--limit', dest='limit', type=int,
  148. help='maximum number of results to return')
  149. parser.add_option('--start', dest='start', type=int,
  150. help='how many changes to skip '
  151. '(starting with the most recent)')
  152. (opt, args) = parser.parse_args(args)
  153. for p in opt.params:
  154. assert '=' in p, '--param is key=value, not "%s"' % p
  155. result = gerrit_util.QueryChanges(
  156. urlparse.urlparse(opt.host).netloc,
  157. list(tuple(p.split('=', 1)) for p in opt.params),
  158. start=opt.start, # Default: None
  159. limit=opt.limit, # Default: None
  160. o_params=opt.o_params, # Default: None
  161. )
  162. logging.info('Change query returned %d changes.', len(result))
  163. write_result(result, opt)
  164. @subcommand.usage('[args ...]')
  165. def CMDrelatedchanges(parser, args):
  166. """Gets related changes for a given change and revision."""
  167. parser.add_option('-c', '--change', type=str, help='change id')
  168. parser.add_option('-r', '--revision', type=str, help='revision id')
  169. (opt, args) = parser.parse_args(args)
  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. @subcommand.usage('[args ...]')
  178. def CMDcreatechange(parser, args):
  179. """Create a new change in gerrit."""
  180. parser.add_option('-s', '--subject', help='subject for change')
  181. parser.add_option('-b',
  182. '--branch',
  183. default='main',
  184. help='target branch for change')
  185. parser.add_option(
  186. '-p',
  187. '--param',
  188. dest='params',
  189. action='append',
  190. help='repeatable field value parameter, format: -p key=value')
  191. parser.add_option('--cc',
  192. dest='cc_list',
  193. action='append',
  194. help='CC address to notify, format: --cc foo@example.com')
  195. (opt, args) = parser.parse_args(args)
  196. for p in opt.params:
  197. assert '=' in p, '--param is key=value, not "%s"' % p
  198. params = list(tuple(p.split('=', 1)) for p in opt.params)
  199. if opt.cc_list:
  200. params.append(('notify_details', {'CC': {'accounts': opt.cc_list}}))
  201. result = gerrit_util.CreateChange(
  202. urlparse.urlparse(opt.host).netloc,
  203. opt.project,
  204. branch=opt.branch,
  205. subject=opt.subject,
  206. params=params,
  207. )
  208. logging.info(result)
  209. write_result(result, opt)
  210. @subcommand.usage('[args ...]')
  211. def CMDchangeedit(parser, args):
  212. """Puts content of a file into a change edit."""
  213. parser.add_option('-c', '--change', type=int, help='change number')
  214. parser.add_option('--path', help='path for file')
  215. parser.add_option('--file', help='file to place at |path|')
  216. (opt, args) = parser.parse_args(args)
  217. with open(opt.file) as f:
  218. data = f.read()
  219. result = gerrit_util.ChangeEdit(
  220. urlparse.urlparse(opt.host).netloc, opt.change, opt.path, data)
  221. logging.info(result)
  222. write_result(result, opt)
  223. @subcommand.usage('[args ...]')
  224. def CMDpublishchangeedit(parser, args):
  225. """Publish a Gerrit change edit."""
  226. parser.add_option('-c', '--change', type=int, help='change number')
  227. parser.add_option('--notify', help='whether to notify')
  228. (opt, args) = parser.parse_args(args)
  229. result = gerrit_util.PublishChangeEdit(
  230. urlparse.urlparse(opt.host).netloc, opt.change, opt.notify)
  231. logging.info(result)
  232. write_result(result, opt)
  233. @subcommand.usage('[args ...]')
  234. def CMDsubmitchange(parser, args):
  235. """Submit a Gerrit change."""
  236. parser.add_option('-c', '--change', type=int, help='change number')
  237. (opt, args) = parser.parse_args(args)
  238. result = gerrit_util.SubmitChange(
  239. urlparse.urlparse(opt.host).netloc, opt.change)
  240. logging.info(result)
  241. write_result(result, opt)
  242. @subcommand.usage('[args ...]')
  243. def CMDchangesubmittedtogether(parser, args):
  244. """Get all changes submitted with the given one."""
  245. parser.add_option('-c', '--change', type=int, help='change number')
  246. (opt, args) = parser.parse_args(args)
  247. result = gerrit_util.GetChangesSubmittedTogether(
  248. urlparse.urlparse(opt.host).netloc, opt.change)
  249. logging.info(result)
  250. write_result(result, opt)
  251. @subcommand.usage('[args ...]')
  252. def CMDgetcommitincludedin(parser, args):
  253. """Retrieves the branches and tags for a given commit."""
  254. parser.add_option('--commit', dest='commit', help='commit hash')
  255. (opt, args) = parser.parse_args(args)
  256. result = gerrit_util.GetCommitIncludedIn(
  257. urlparse.urlparse(opt.host).netloc, opt.project, opt.commit)
  258. logging.info(result)
  259. write_result(result, opt)
  260. @subcommand.usage('[args ...]')
  261. def CMDsetbotcommit(parser, args):
  262. """Sets bot-commit+1 to a bot generated change."""
  263. parser.add_option('-c', '--change', type=int, help='change number')
  264. (opt, args) = parser.parse_args(args)
  265. result = gerrit_util.SetReview(
  266. urlparse.urlparse(opt.host).netloc,
  267. opt.change,
  268. labels={'Bot-Commit': 1},
  269. ready=True)
  270. logging.info(result)
  271. write_result(result, opt)
  272. @subcommand.usage('[args ...]')
  273. def CMDsetlabel(parser, args):
  274. """Sets a label to a specific value on a given change."""
  275. parser.add_option('-c', '--change', type=int, help='change number')
  276. parser.add_option('-l',
  277. '--label',
  278. nargs=2,
  279. metavar=('label_name', 'label_value'))
  280. (opt, args) = parser.parse_args(args)
  281. result = gerrit_util.SetReview(urlparse.urlparse(opt.host).netloc,
  282. opt.change,
  283. labels={opt.label[0]: opt.label[1]})
  284. logging.info(result)
  285. write_result(result, opt)
  286. @subcommand.usage('')
  287. def CMDabandon(parser, args):
  288. """Abandons a Gerrit change."""
  289. parser.add_option('-c', '--change', type=int, help='change number')
  290. parser.add_option('-m', '--message', default='', help='reason for abandoning')
  291. (opt, args) = parser.parse_args(args)
  292. assert opt.change, "-c not defined"
  293. result = gerrit_util.AbandonChange(
  294. urlparse.urlparse(opt.host).netloc,
  295. opt.change, opt.message)
  296. logging.info(result)
  297. write_result(result, opt)
  298. @subcommand.usage('')
  299. def CMDmass_abandon(parser, args):
  300. """Mass abandon changes
  301. Abandons CLs that match search criteria provided by user. Before any change is
  302. actually abandoned, user is presented with a list of CLs that will be affected
  303. if user confirms. User can skip confirmation by passing --force parameter.
  304. The script can abandon up to 100 CLs per invocation.
  305. Examples:
  306. gerrit_client.py mass-abandon --host https://HOST -p 'project=repo2'
  307. gerrit_client.py mass-abandon --host https://HOST -p 'message=testing'
  308. gerrit_client.py mass-abandon --host https://HOST -p 'is=wip' -p 'age=1y'
  309. """
  310. parser.add_option('-p',
  311. '--param',
  312. dest='params',
  313. action='append',
  314. default=[],
  315. help='repeatable query parameter, format: -p key=value')
  316. parser.add_option('-m', '--message', default='', help='reason for abandoning')
  317. parser.add_option('-f',
  318. '--force',
  319. action='store_true',
  320. help='Don\'t prompt for confirmation')
  321. opt, args = parser.parse_args(args)
  322. for p in opt.params:
  323. assert '=' in p, '--param is key=value, not "%s"' % p
  324. search_query = list(tuple(p.split('=', 1)) for p in opt.params)
  325. if not any(t for t in search_query if t[0] == 'owner'):
  326. # owner should always be present when abandoning changes
  327. search_query.append(('owner', 'me'))
  328. search_query.append(('status', 'open'))
  329. logging.info("Searching for: %s" % search_query)
  330. host = urlparse.urlparse(opt.host).netloc
  331. result = gerrit_util.QueryChanges(
  332. host,
  333. search_query,
  334. # abandon at most 100 changes as not all Gerrit instances support
  335. # unlimited results.
  336. limit=100,
  337. )
  338. if len(result) == 0:
  339. logging.warn("Nothing to abandon")
  340. return
  341. logging.warn("%s CLs match search query: " % len(result))
  342. for change in result:
  343. logging.warn("[ID: %d] %s" % (change['_number'], change['subject']))
  344. if not opt.force:
  345. q = input(
  346. 'Do you want to move forward with abandoning? [y to confirm] ').strip()
  347. if q not in ['y', 'Y']:
  348. logging.warn("Aborting...")
  349. return
  350. for change in result:
  351. logging.warning("Abandoning: %s" % change['subject'])
  352. gerrit_util.AbandonChange(host, change['id'], opt.message)
  353. logging.warning("Done")
  354. class OptionParser(optparse.OptionParser):
  355. """Creates the option parse and add --verbose support."""
  356. def __init__(self, *args, **kwargs):
  357. optparse.OptionParser.__init__(self, *args, version=__version__, **kwargs)
  358. self.add_option(
  359. '--verbose', action='count', default=0,
  360. help='Use 2 times for more debugging info')
  361. self.add_option('--host', dest='host', help='Url of host.')
  362. self.add_option('--project', dest='project', help='project name')
  363. self.add_option(
  364. '--json_file', dest='json_file', help='output json filepath')
  365. def parse_args(self, args=None, values=None):
  366. options, args = optparse.OptionParser.parse_args(self, args, values)
  367. # Host is always required
  368. assert options.host, "--host not defined."
  369. levels = [logging.WARNING, logging.INFO, logging.DEBUG]
  370. logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
  371. return options, args
  372. def main(argv):
  373. if sys.hexversion < 0x02060000:
  374. print('\nYour python version %s is unsupported, please upgrade.\n'
  375. % (sys.version.split(' ', 1)[0],),
  376. file=sys.stderr)
  377. return 2
  378. dispatcher = subcommand.CommandDispatcher(__name__)
  379. return dispatcher.execute(OptionParser(), argv)
  380. if __name__ == '__main__':
  381. # These affect sys.stdout so do it outside of main() to simplify mocks in
  382. # unit testing.
  383. fix_encoding.fix_encoding()
  384. setup_color.init()
  385. try:
  386. sys.exit(main(sys.argv[1:]))
  387. except KeyboardInterrupt:
  388. sys.stderr.write('interrupted\n')
  389. sys.exit(1)