gerrit_client.py 21 KB

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