gerrit_client.py 18 KB

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