gerrit_client.py 15 KB

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