gerrit_client.py 19 KB

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