api.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. # Copyright 2013 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. from recipe_engine import recipe_api
  5. class GerritApi(recipe_api.RecipeApi):
  6. """Module for interact with Gerrit endpoints"""
  7. def __init__(self, *args, **kwargs):
  8. super(GerritApi, self).__init__(*args, **kwargs)
  9. self._changes_target_branch_cache = {}
  10. def __call__(self, name, cmd, infra_step=True, **kwargs):
  11. """Wrapper for easy calling of gerrit_utils steps."""
  12. assert isinstance(cmd, (list, tuple))
  13. prefix = 'gerrit '
  14. env = self.m.context.env
  15. env.setdefault('PATH', '%(PATH)s')
  16. env['PATH'] = self.m.path.pathsep.join([
  17. env['PATH'], str(self.repo_resource())])
  18. with self.m.context(env=env):
  19. return self.m.python(prefix + name,
  20. self.repo_resource('gerrit_client.py'),
  21. cmd,
  22. infra_step=infra_step,
  23. venv=True,
  24. **kwargs)
  25. def call_raw_api(self,
  26. host,
  27. path,
  28. method=None,
  29. body=None,
  30. accept_statuses=None,
  31. name=None,
  32. **kwargs):
  33. """Call an arbitrary Gerrit API that returns a JSON response.
  34. Returns:
  35. The JSON response data.
  36. """
  37. args = [
  38. 'rawapi', '--host', host, '--path', path, '--json_file',
  39. self.m.json.output()
  40. ]
  41. if method:
  42. args.extend(['--method', method])
  43. if body:
  44. args.extend(['--body', self.m.json.dumps(body)])
  45. if accept_statuses:
  46. args.extend(
  47. ['--accept_status', ','.join(str(i) for i in accept_statuses)])
  48. step_name = name or 'call_raw_api (%s)' % path
  49. step_result = self(step_name, args, **kwargs)
  50. return step_result.json.output
  51. def create_gerrit_branch(self, host, project, branch, commit, **kwargs):
  52. """Creates a new branch from given project and commit
  53. Returns:
  54. The ref of the branch created
  55. """
  56. args = [
  57. 'branch',
  58. '--host', host,
  59. '--project', project,
  60. '--branch', branch,
  61. '--commit', commit,
  62. '--json_file', self.m.json.output()
  63. ]
  64. step_name = 'create_gerrit_branch (%s %s)' % (project, branch)
  65. step_result = self(step_name, args, **kwargs)
  66. ref = step_result.json.output.get('ref')
  67. return ref
  68. def create_gerrit_tag(self, host, project, tag, commit, **kwargs):
  69. """Creates a new tag at the given commit.
  70. Returns:
  71. The ref of the tag created.
  72. """
  73. args = [
  74. 'tag',
  75. '--host', host,
  76. '--project', project,
  77. '--tag', tag,
  78. '--commit', commit,
  79. '--json_file', self.m.json.output()
  80. ]
  81. step_name = 'create_gerrit_tag (%s %s)' % (project, tag)
  82. step_result = self(step_name, args, **kwargs)
  83. ref = step_result.json.output.get('ref')
  84. return ref
  85. # TODO(machenbach): Rename to get_revision? And maybe above to
  86. # create_ref?
  87. def get_gerrit_branch(self, host, project, branch, **kwargs):
  88. """Gets a branch from given project and commit
  89. Returns:
  90. The revision of the branch
  91. """
  92. args = [
  93. 'branchinfo',
  94. '--host', host,
  95. '--project', project,
  96. '--branch', branch,
  97. '--json_file', self.m.json.output()
  98. ]
  99. step_name = 'get_gerrit_branch (%s %s)' % (project, branch)
  100. step_result = self(step_name, args, **kwargs)
  101. revision = step_result.json.output.get('revision')
  102. return revision
  103. def get_change_description(self,
  104. host,
  105. change,
  106. patchset,
  107. timeout=None,
  108. step_test_data=None):
  109. """Gets the description for a given CL and patchset.
  110. Args:
  111. host: URL of Gerrit host to query.
  112. change: The change number.
  113. patchset: The patchset number.
  114. Returns:
  115. The description corresponding to given CL and patchset.
  116. """
  117. ri = self.get_revision_info(host, change, patchset, timeout, step_test_data)
  118. return ri['commit']['message']
  119. def get_revision_info(self,
  120. host,
  121. change,
  122. patchset,
  123. timeout=None,
  124. step_test_data=None):
  125. """
  126. Returns the info for a given patchset of a given change.
  127. Args:
  128. host: Gerrit host to query.
  129. change: The change number.
  130. patchset: The patchset number.
  131. Returns:
  132. A dict for the target revision as documented here:
  133. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
  134. """
  135. assert int(change), change
  136. assert int(patchset), patchset
  137. step_test_data = step_test_data or (
  138. lambda: self.test_api.get_one_change_response_data(change_number=change,
  139. patchset=patchset))
  140. cls = self.get_changes(host,
  141. query_params=[('change', str(change))],
  142. o_params=['ALL_REVISIONS', 'ALL_COMMITS'],
  143. limit=1,
  144. timeout=timeout,
  145. step_test_data=step_test_data)
  146. cl = cls[0] if len(cls) == 1 else {'revisions': {}}
  147. for ri in cl['revisions'].values():
  148. # TODO(tandrii): add support for patchset=='current'.
  149. if str(ri['_number']) == str(patchset):
  150. return ri
  151. raise self.m.step.InfraFailure(
  152. 'Error querying for CL description: host:%r change:%r; patchset:%r' % (
  153. host, change, patchset))
  154. def get_changes(self, host, query_params, start=None, limit=None,
  155. o_params=None, step_test_data=None, **kwargs):
  156. """Queries changes for the given host.
  157. Args:
  158. * host: URL of Gerrit host to query.
  159. * query_params: Query parameters as list of (key, value) tuples to form a
  160. query as documented here:
  161. https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators
  162. * start: How many changes to skip (starting with the most recent).
  163. * limit: Maximum number of results to return.
  164. * o_params: A list of additional output specifiers, as documented here:
  165. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
  166. * step_test_data: Optional mock test data for the underlying gerrit client.
  167. Returns:
  168. A list of change dicts as documented here:
  169. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
  170. """
  171. args = [
  172. 'changes',
  173. '--host', host,
  174. '--json_file', self.m.json.output()
  175. ]
  176. if start:
  177. args += ['--start', str(start)]
  178. if limit:
  179. args += ['--limit', str(limit)]
  180. for k, v in query_params:
  181. args += ['-p', '%s=%s' % (k, v)]
  182. for v in (o_params or []):
  183. args += ['-o', v]
  184. if not step_test_data:
  185. step_test_data = lambda: self.test_api.get_one_change_response_data()
  186. return self(
  187. kwargs.pop('name', 'changes'),
  188. args,
  189. step_test_data=step_test_data,
  190. **kwargs
  191. ).json.output
  192. def get_related_changes(self, host, change, revision='current', step_test_data=None):
  193. """Queries related changes for a given host, change, and revision.
  194. Args:
  195. * host: URL of Gerrit host to query.
  196. * change: The change-id of the change to get related changes for as
  197. documented here:
  198. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
  199. * revision: The revision-id of the revision to get related changes for as
  200. documented here:
  201. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-id
  202. This defaults to current, which names the most recent patch set.
  203. * step_test_data: Optional mock test data for the underlying gerrit client.
  204. Returns:
  205. A related changes dictionary as documented here:
  206. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#related-changes-info
  207. """
  208. args = [
  209. 'relatedchanges',
  210. '--host',
  211. host,
  212. '--change',
  213. change,
  214. '--revision',
  215. revision,
  216. '--json_file',
  217. self.m.json.output(),
  218. ]
  219. if not step_test_data:
  220. step_test_data = lambda: self.test_api.get_related_changes_response_data()
  221. return self('relatedchanges', args,
  222. step_test_data=step_test_data).json.output
  223. def abandon_change(self, host, change, message=None, name=None,
  224. step_test_data=None):
  225. args = [
  226. 'abandon',
  227. '--host', host,
  228. '--change', int(change),
  229. '--json_file', self.m.json.output(),
  230. ]
  231. if message:
  232. args.extend(['--message', message])
  233. if not step_test_data:
  234. step_test_data = lambda: self.test_api.get_one_change_response_data(
  235. status='ABANDONED', _number=str(change))
  236. return self(
  237. name or 'abandon',
  238. args,
  239. step_test_data=step_test_data,
  240. ).json.output
  241. def set_change_label(self,
  242. host,
  243. change,
  244. label_name,
  245. label_value,
  246. name=None,
  247. step_test_data=None):
  248. args = [
  249. 'setlabel', '--host', host, '--change',
  250. int(change), '--json_file',
  251. self.m.json.output(), '-l', label_name, label_value
  252. ]
  253. return self(
  254. name or 'setlabel',
  255. args,
  256. step_test_data=step_test_data,
  257. ).json.output
  258. def move_changes(self,
  259. host,
  260. project,
  261. from_branch,
  262. to_branch,
  263. step_test_data=None):
  264. args = [
  265. 'movechanges', '--host', host, '-p',
  266. 'project=%s' % project, '-p',
  267. 'branch=%s' % from_branch, '-p', 'status=open', '--destination_branch',
  268. to_branch, '--json_file',
  269. self.m.json.output()
  270. ]
  271. if not step_test_data:
  272. step_test_data = lambda: self.test_api.get_one_change_response_data(
  273. branch=to_branch)
  274. return self(
  275. 'move changes',
  276. args,
  277. step_test_data=step_test_data,
  278. ).json.output
  279. def update_files(self,
  280. host,
  281. project,
  282. branch,
  283. new_contents_by_file_path,
  284. commit_msg,
  285. params=frozenset(['status=NEW']),
  286. submit=False,
  287. submit_later=False):
  288. """Update a set of files by creating and submitting a Gerrit CL.
  289. Args:
  290. * host: URL of Gerrit host to name.
  291. * project: Gerrit project name, e.g. chromium/src.
  292. * branch: The branch to land the change, e.g. main
  293. * new_contents_by_file_path: Dict of the new contents with file path as
  294. the key.
  295. * commit_msg: Description to add to the CL.
  296. * params: A list of additional ChangeInput specifiers, with format
  297. 'key=value'.
  298. * submit: Should land this CL instantly.
  299. * submit_later: If this change has related CLs, we may want to commit
  300. them in a chain. So only set Bot-Commit+1, making it ready for
  301. submit together. Ignored if submit is True.
  302. Returns:
  303. A ChangeInfo dictionary as documented here:
  304. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#create-change
  305. Or if the change is submitted, here:
  306. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submit-change
  307. """
  308. assert len(new_contents_by_file_path
  309. ) > 0, 'The dict of file paths should not be empty.'
  310. command = [
  311. 'createchange',
  312. '--host',
  313. host,
  314. '--project',
  315. project,
  316. '--branch',
  317. branch,
  318. '--subject',
  319. commit_msg,
  320. '--json_file',
  321. self.m.json.output(),
  322. ]
  323. for p in params:
  324. command.extend(['-p', p])
  325. step_result = self('create change at (%s %s)' % (project, branch), command)
  326. change = int(step_result.json.output.get('_number'))
  327. step_result.presentation.links['change %d' %
  328. change] = '%s/#/q/%d' % (host, change)
  329. with self.m.step.nest('update contents in CL %d' % change):
  330. for path, content in new_contents_by_file_path.items():
  331. _file = self.m.path.mkstemp()
  332. self.m.file.write_raw('store the new content for %s' % path, _file,
  333. content)
  334. self('edit file %s' % path, [
  335. 'changeedit',
  336. '--host',
  337. host,
  338. '--change',
  339. change,
  340. '--path',
  341. path,
  342. '--file',
  343. _file,
  344. ])
  345. self('publish edit', [
  346. 'publishchangeedit',
  347. '--host',
  348. host,
  349. '--change',
  350. change,
  351. ])
  352. if submit or submit_later:
  353. self('set Bot-Commit+1 for change %d' % change, [
  354. 'setbotcommit',
  355. '--host',
  356. host,
  357. '--change',
  358. change,
  359. ])
  360. if submit:
  361. submit_cmd = [
  362. 'submitchange',
  363. '--host',
  364. host,
  365. '--change',
  366. change,
  367. '--json_file',
  368. self.m.json.output(),
  369. ]
  370. step_result = self('submit change %d' % change, submit_cmd)
  371. return step_result.json.output